Changelog
Source:NEWS.md
rFIA v1.2.0
This version implements a variety of updates to model estimation functions after a complete validation of the package’s functionality. An extensive suite of unit tests are implemented for checking rFIA estimates with estimates from EVALIDator to ensure consistency of rFIA with updates in FIADB. This validation assessment fixed multiple bugs, particularly related to the reporting of sample sizes that were not always consistent dependent on different filtering criteria implemented in the estimation functions. Updates are broken down in the following based on specific functions.
Full details on this validation are provided in the development version of rFIA on GitHub in the core_references/validation directory. Note that Claude Code was used to aid in the validation assessment, with code verified by Jeff Doser.
customPSE()
- Fixed a bug in
customPSE()(GitHub issue #47) where a spatial mask (or any multi-state call) spanning states with differentmostRecentevaluation years returned one row per state/year instead of a single combined estimate, unlikearea()/tpa()/etc. on the same masked data. Root cause:customPSE()checkeddbforclipFIA(mostRecent = TRUE)’s marker after an internal helper had already pareddbdown to only the named FIA tables, which silently dropped that marker – so the step that relabels differing per-state “most recent” years to a common year before combining never ran. Every other estimator dispatcher checks this before any table-paring happens, so none of them were affected. - Fixed a bug in
customPSE()wherenPlots_x/nPlots_ywere inflated to the full forested-plot count instead of the true number of non-zero contributing plots, wheneverx/ywas a tree-based list (e.g. thetreeList = TRUEoutput oftpa()/volume()). Point estimates and standard errors were unaffected – this was purely a plot-count/reporting bug, but it mattered becausecustomPSE.RddocumentsnPlots_x/nPlots_yas the count of non-zero plots, used as the degrees of freedom for a t-based confidence interval. Root cause:sumToPlot()(R/util.R), shared by every estimator, did not filter out the phantomTREE_BASIS/AREA_BASIS = NArows that atreeList/condListoutput necessarily includes for forested conditions with zero qualifying trees – each*Starter.Rfile already filters these before its own population-estimate call, butcustomPSE()callssumToPlot()directly on user-supplied data. Confirmed againsttpa()/volume()/area()across four states (one per FIA region). - Fixed a
dplyr::across()deprecation warning incustomPSE()(...was passed through to.fnsrather than wrapped in an anonymous function, deprecated as of dplyr 1.1.0).
Changes to multiple functions
-
tpa(),biomass(),carbon(),volume(),growMort(),vitalRates(),diversity(),fsi(),area(),areaChange(),standStruct(), andseedling()now warn whengrpByincludesSUBP. Unlike every othergrpByvariable these functions support (species, size class, ownership group, etc.), which partition trees/conditions within the same shared sampled area,SUBPdenotes a physically distinct sub-area of the plot – each subplot covers different ground. Grouping bySUBPstill produces a mathematically valid partition of the plot-level per-acre estimate (each subplot’s value is its share of the total, and the four values sum back to the ungrouped estimate exactly), but it is not a re-weighted, subplot-local density, since the area denominator is not re-weighted to match each subplot’s own area. Documented in each function’sDetailssection (issue #31). This warning is used to reflect thatrFIAis not designed for extracting data for each subplot and subsequently using that data within a model-based estimator. - Fixed a bug in the shared internal utility used to evaluate a user-supplied
areaDomainwhere the plots/conditions used to evaluate the domain expression were hard-coded to forest land (PLOT_STATUS_CD == 1/COND_STATUS_CD == 1), regardless oflandType. This causedarea()(andareaChange()) to silently return zero area for any combination of a non-forestlandType(e.g.'water','non-forest','all') with anareaDomainfilter, instead of applying the filter and returning the correctly restricted estimate. This is a rare use case. - Fixed a bug where
treeType = 'dead'did not require dead trees to meet the “standing dead” tally-tree criteria (STANDING_DEAD_CD == 1), instead counting all trees withSTATUSCD == 2regardless of whether they were still standing. This inflatedtreeType = 'dead'estimates in states with a meaningful number of down or broken dead trees recorded in the tree table (e.g. North Carolina, where the estimate was roughly 4x too high). This affects every function that supportstreeType:tpa(),diversity(),biomass(),volume(), andfsi(). As a consequence,treeType = 'all'(which includes every tree regardless of status) is no longer equal totreeType = 'live'plustreeType = 'dead', since'all'still includes the non-standing dead trees that'dead'now excludes.
growMort() and vitalRates()
- Fixed a bug in
growMort()andvitalRates()wherebySizeClass = TRUEsilently dropped removed (harvested) and dead trees whose current-cycle diameter went unmeasured – common for trees that are too decayed, broken, or fully removed to measure at the time of remeasurement.sizeClasswas computed from the tree’s current diameter and any row with a missing value was dropped before it was ever joined to the growth/mortality/removal tables, even though those same tables already provide a usable midpoint or begin diameter for exactly these trees. Confirmed against OR, where this undercutgrowMort()’s harvested-basal-area estimate by up to 99% and its mortality estimate by up to 25% whenbySizeClass = TRUE;bySizeClass = TRUEtotals (summed back across size classes) now matchbySizeClass = FALSEexactly in both functions (issue #40). -
growMort()now returnsnPlots_RECR,nPlots_MORT,nPlots_REMV, andnPlots_GROW– separate non-zero-plot counts for recruitment, mortality, harvest removal, and survivor growth, respectively. The first three were already documented ingrowMort.Rdbut never actually implemented;nPlots_GROWis a new addition alongside them for consistency. Previously, only a single genericnPlots_TREE(count of plots contributing to any of recruitment/mortality/removal/survivor-growth combined) was returned, which understates the degrees of freedom appropriate for a t-based confidence interval on an individualMORT_*/REMV_*/RECR_*/GROW_*rate.nPlots_TREEis unchanged. Confirmed against EVALIDator’s plot counts for the mortality/harvest-removal attributes (exact match). - Fixed a bug in
growMort()whereGROW_*/CHNG_*(survivor growth / net change) were computed incorrectly for every state variable except the defaultTPA–BAA, volume, biomass, and carbon outputs were all affected. The previous-period population total was reconstructed using each departed tree’s midpoint measurement (correct for the separately-reportedMORT_*/REMV_*columns) instead of its begin measurement, which EVALIDator’s own growth-accounting definition of “net growth” requires; a second, related bug caused mismatchedNAhandling across theRECR/MORT/REMV/GROWcolumns for volume-based state variables (NETVOL,SAWVOL,SAWVOL_BF), whose underlying FIADB columns are undefined for some trees below merchantability thresholds.MORT_*/REMV_*themselves, and every output under the defaultstateVar = 'TPA', were unaffected. This is likely related to the “growMort()reporting zero survivor growth” bug fixed in v1.1.1. - Updated
growMort()where state variables derived from tree biomass (BIO_AG,BIO_BG,BIO,CARB_AG,CARB_BG,CARB) to now be reported in pounds instead of short tons/acre. This is for consistency with other estimation functions across the package. - Fixed a bug in
growMort()where anareaDomainfilter was evaluated against a tree’s previous remeasurement condition rather than its current one, understatingMORT_*/REMV_*/RECR_*estimates in states with meaningful physiographic-class turnover between remeasurements (up to -6% in checks against EVALIDator). This is the same bug already fixed invitalRates(). - Fixed a bug in
growMort()where theSUBP_COND_CHNG_MTRX-based growth-accounting area-change calculation hardcodedSUBPTYP == 1, silently discarding area-change information for any condition measured on the macroplot and understatinglandType = 'forest'estimates (andnPlots_AREA) in macroplot-heavy states (e.g. Pacific Northwest). This is the same bug already fixed invitalRates(). - Fixed a bug in
vitalRates()whereSAWVOL_GROW/SAWVOL_GROW_AC(sawlog board-foot volume growth) was computed from the same growing-stock growth-accounting component used for the other four growth metrics (DIA_GROW,BA_GROW,NETVOL_GROW,BIO_GROW), rather than the sawtimber-specific component EVALIDator’s sawlog-volume growth attributes are actually defined against – the same distinctiongrowMort()already makes for its ownSAWVOL/SAWVOL_BFstate variables, independent oftreeType. This under- or over-statedSAWVOL_GROW_ACby roughly 0.3-3% depending on state. Point estimates and sampling errors for the other four growth metrics were not affected. - Updated
vitalRates()where anareaDomainrestriction (e.g.PHYSCLCD %in% 21:29) was applied to tree-level growth using the previous measurement’s condition instead of the current one, while the area (denominator) side already correctly used the current condition – creating a small, state-dependent mismatch (worse in states with more physiographic-class turnover between remeasurements) whenever a plot’sareaDomain-relevant classification changed between visits. Point estimates forareaDomain-restricted calls were affected; unrestricted calls were not. - Fixed a bug in
vitalRates()wherelandType = 'forest'silently dropped all area-change information for any condition whose proportion was measured on the macroplot (COND.PROP_BASIS == 'MACR') rather than the standard subplot – the internal area-change join hardcodedSUBP_COND_CHNG_MTRX.SUBPTYP == 1, when the FIA Population Estimation User Guide’s own growth-accounting methodology requiresSUBPTYP == 3for macroplot-basis conditions. This was invisible in states where forest conditions are exclusively subplot-basis (confirmed for RI, NC, CO), but caused a small, consistent undercount ofBIO_GROW_AC(~-0.1%) andnPlots_AREA(~-0.6%) in Pacific/Western states that commonly use macroplot sampling (confirmed for OR, CA, WA). A residual, smaller discrepancy specific tolandType = 'timber'exists with EVALIDator but is kept different inrFIA. See details incore_referencesfor more details. - Fixed a bug in
vitalRates()wherenPlots_TREEdid not reflect restrictions imposed bytreeDomainat all – even atreeDomainmatching zero trees leftnPlots_TREEunchanged from the unrestricted value. Every row of abySpecies = TRUEcall reported the same, unrestrictednPlots_TREEregardless of how common that species actually was, defeating its use as the degrees of freedom for a t-based confidence interval. Root cause: the tree list’s plot-count filter depended only on whether a tree had a valid growth-accounting record for the currentlandType/treeType(via FIA’s precomputedTREE_GRM_COMPONENTcolumns), not on whether the user’streeDomain/areaDomainindicator (tDI) actually matched –tDIonly zeroed the growth values for non-matching trees, without excluding them from the plot count. Point estimates and sampling errors were not affected. - Fixed a bug in
vitalRates()wherenPlots_AREAdid not reflect restrictions imposed bylandTypeorareaDomainat all (the same class of bug already fixed intpa(),area(),carbon(),biomass(),volume(),dwm(),invasive(),seedling(),standStruct(),diversity(), andvegStruct(); see above), including the same spurious/empty-result-with-warning edge case when a restriction matched no data. Point estimates and sampling errors were not affected. - Fixed a bug affecting
vitalRates()andgrowMort()undermethod = 'SMA'/'LMA'/'EMA'where growth/mortality totals (and every ratio derived from them, e.g.BIO_GROW_AC,MORT_TPA) were inflated by roughly the number of remeasurement panels in the evaluation window – up to ~9x in testing. Root cause: a shared internal utility (sumToEU()) that combines per-panel moving-average estimates into a single population estimate grouped the tree/growth side of the calculation by an extra, panel-varying column that should not have been part of the grouping key, preventing panels from actually being combined;vitalRates()/growMort()’s internal need to compute two related population estimates and join them together turned this into a many-to-many join that squared the row count. Every other estimator was unaffected in practice (a separate summation step downstream happened to still produce the correct total), which is why this was not caught until now.method = 'TI'and'ANNUAL'were not affected.
fsi()
- Fixed a bug in
fsi()where the plot-level remeasurement interval (REMPER) was incorrectly multiplied by the subplot/microplot/macroplot nonresponse adjustment factor before being used as a grouping key to recombine a plot’s per-plot-basis rows back into a single row. Because that adjustment factor differs by plot basis, a single plot’s rows ended up with differentREMPERvalues and so failed to recombine, leaving the same physical plot represented by multiple rows. Tree-density sums still totaled correctly across those extra rows, but the subsequent area join (not keyed onREMPER) re-attached a full copy of that plot’s forest-area weight to each spurious row, inflating the area denominator relative to the tree-count numerator. Values frombyPlot = TRUEwere not affected, since they do not go through this code path. - Fixed the informative prior on the intercept of
fsi()’s maximum size-density curve (Bayesian quantile regression,inst/extdata/qrLM.jag/qrLMM.jag) to match Stanke et al. (2021): mean 7, not 6. The slope prior was already correct. - Fixed
fsi()’s exclusion of disturbed/treated plots from the maximum size-density curve calibration set to match Stanke et al. (2021): a plot showing evidence of disturbance or (non-natural-regen) silvicultural treatment is now excluded, rather than requiring both simultaneously. Previously, a plot with significant disturbance but no recorded follow-up treatment (e.g. an untreated wildfire) was incorrectly retained in the calibration set. - Removed the
totalsargument fromfsi(). It had no effect on the returned output (both branches of thetotals-dependent code returned identical columns) despite being documented as adding raw population totals; every other estimator’stotalsargument works as documented;fsi()’s did not and has been removed rather than implemented, sinceFSI/PERC_FSI/PREV_RD/CURR_RDare already ratio quantities with no natural population-total analog. - Fixed a bug where
fsi(areaDomain = ...)crashed with"replacement has 1 row, data has 0"when theareaDomain(orlandType) restricted the population to zero plots, instead of returning a clean empty result.byPlot = FALSEnow returns a 0-row result in this case (or when atreeDomainmatches zero trees, which previously returned a spurious 1-row result ofNaNs instead of an empty result);byPlot = TRUEkeeps all plot rows withFSI = 0/NA, consistent with howtpa()andvitalRates()already handle an empty domain. - Fixed a
dplyr::across()deprecation warning raised byfsi(method = 'ANNUAL').
vegStruct()
- Updated
vegStruct()to now include region-specificGROWTH_HABIT_CDs as opposed to only including the 5 core national vegetation growth-habit codes. - Fixed a bug in
vegStruct()wherebyPlot = TRUE’s per-plot cover estimate (PROP_COVER) usedmean(cover, na.rm = TRUE)across a layer/growth-habit combination’s subplot-level cover values, which treats a subplot where that combination wasn’t recorded as a missing observation to exclude from the average rather than a real 0%-cover observation to include (the same bug already fixed ininvasive()’sbyPlotbranch). This inflatedPROP_COVERby up to 4x for a combination recorded on fewer than all 4 subplots – the normal case for patchy vegetation. Fixed by dividing by a fixed 4 subplots instead, matching the population-estimate branch’s own formula.byPlot = TRUEwas the only affected output; the main population-levelCOVER_PCTwas not affected. - Fixed a bug in
vegStruct()wherenPlots_AREAdid not reflect restrictions imposed bylandTypeorareaDomain(the same class of bug already fixed intpa(),area(),carbon(),biomass(),volume(),dwm(),invasive(),seedling(),standStruct(), anddiversity(); see below), and anareaDomain/landTyperestriction matching no data could produce a spurious result instead of a clean empty one. Point estimates and sampling errors were not affected by either fix. - Fixed a bug where
method = 'SMA'/'LMA'/'EMA'/'ANNUAL'could error with"replacement has length zero"instead of returning a result (or the existing “bad stratification” warning, when a stratum is genuinely too small to merge). This is not specific tovegStruct(): the underlying shared utility that pools too-small strata together (R/util.R, used by everysumToEU()-based estimator) picked a stratum’s cross-year merge partner without accounting for a stratum whoseINVYRis unknown – reachable whenever a function restricts its plot universe before computing population weights (asvegStruct()andinvasive()do, for their P2-ancillary-protocol sampling restrictions), which leaves some strata with no real year of their own.tpa()/area()/etc., which don’t restrict their plot universe this way, were confirmed unaffected – their output is unchanged.
diversity()
- Fixed a bug in
diversity()where grouping by aTREE-table variable (bySizeClass = TRUE, or a user-suppliedgrpByreferencing aTREEcolumn, e.g. species group) corrupted the area denominator: each forest condition’s area was collapsed into whichever one grouping bin happened to be encountered first, instead of correctly contributing to every bin its trees actually belong to. This could push alpha-level Shannon’s Equitability (Eh_a) above 1, which is mathematically impossible under its own formula. Fixed by separating the area-only grouping columns from the full grouping columns internally (matchingtpa()’s existingaGrpBy/grpBysplit), so aTREE-level grouping variable no longer fragments the area total.grpByrestricted toPLOT/CONDcolumns (e.g.OWNGRPCD) was not affected. - Fixed a bug in
diversity()where anareaDomain/landTyperestriction matching no data produced a spurious result (H = S = 0with a"no non-missing arguments to max"warning) instead of a clean empty result.nPlots_AREAalso did not reflect restrictions imposed bylandTypeorareaDomain(the same class of bug already fixed intpa(),area(),carbon(),biomass(),volume(),dwm(),invasive(),seedling(), andstandStruct(); see below). Point estimates and sampling errors were not affected by either fix.
standStruct()
- Fixed a bug in
standStruct()where a forest condition with zero qualifying live trees (e.g. a young/sparse/non-stocked stand) survives the internal tree-list join as a phantom row indistinguishable from any other such condition on the same plot by(PLT_CN, SUBP, TREE)alone (SUBP/TREEare bothNA). Whenever a single plot had two or more zero-tree forest conditions,distinct(PLT_CN, SUBP, TREE)collapsed them into one, silently dropping every zero-tree condition’s area past the first from its stand structural stage (“mosaic”) classification – even though that area still correctly counted in the area total, soCOVER_PCTsummed across all four structural stages fell short of 100% (e.g. North Carolina: 99.986% instead of 100%). Fixed by addingCONDIDto the deduplication key. - Fixed a bug in
standStruct()where anareaDomain/landTyperestriction matching no data produced a spurious single-row'mosaic'result (with a"no non-missing arguments to max"warning) instead of a clean empty result, because the internal structural-stage classification step could still assign a fallback'mosaic'label to a plot with no real qualifying conditions.nPlots_AREAalso did not reflect restrictions imposed bylandTypeorareaDomain(the same class of bug already fixed intpa(),area(),carbon(),biomass(),volume(),dwm(),invasive(), andseedling(); see below). Point estimates and sampling errors were not affected by either fix.
seedling()
- Fixed a bug in
seedling()where the tree list’sdistinct(PLT_CN, SUBP, SPCD)deduplication key omittedCONDID. UnlikeTREE,SEEDLINGhas no per-stem ID –TPA_UNADJis already a count pre-aggregated to thePLT_CN/SUBP/CONDID/SPCDgrain by FIA – so whenever a subplot straddled two conditions and the same species had seedlings recorded under both, this silently discarded one condition’s count entirely. Confirmed on real data (North Carolina): this undercounted statewide seedling TPA by ~0.2%, moving it from 1228.331 to the correct 1230.528 (matching EVALIDator exactly) after the fix. Small/simple states (e.g. Rhode Island) rarely hit the triggering condition and were unaffected. - Fixed a bug in
seedling()wherenPlots_TREEcounted every forest plot rather than just plots with at least one live seedling actually recorded, because itsTREE_BASIScolumn (always'MICR'for seedlings) couldn’t detect a phantom “no seedling” row the waytpa()’sDIA-derivedTREE_BASISdoes. Point estimates and sampling errors were not affected, only the reported plot count. - Fixed a bug in
seedling()wherenPlots_AREAdid not reflect restrictions imposed bylandTypeorareaDomain(the same class of bug already fixed intpa(),area(),carbon(),biomass(),volume(),dwm(), andinvasive(); see below –seedling()was the one remaining estimator missing this fix). Point estimates and sampling errors were not affected.
invasive()
- Updated the internal
REF_PLANT_DICTIONARYreference table (used byinvasive()to attach a scientific/common name to each invasive species code) from an out-of-date snapshot to the current version provided by FIA. The previous version only included species-level PLANTS codes; genus-level codes (used whenever field crews identify an invasive plant’s genus but not its exact species, e.g.LIGUS2for Ligustrum spp., a major invasive shrub genus in the southeastern US) were entirely absent. Because the name columns are part ofinvasive()’s internal grouping and its final output step drops any row with a missing group value, every genus-level species was silently dropped from the output entirely – not just its name, but its realCOVER_PCTdata. This affected up to 36% of raw invasive-species records in the states checked (North Carolina).tpa()/biomass()/volume()’s analogous tree-species reference table was checked and does not have this problem. - Fixed a bug in
invasive()wherebyPlot = TRUE‘s per-plot cover estimate (PROP_INV_COVER) usedmean(cover, na.rm = TRUE)across a species’ subplot-level cover values, which treats a subplot where the species wasn’t recorded as a missing observation to exclude from the average rather than a real 0%-cover observation to include. This inflatedPROP_INV_COVERby up to 16x for a species detected on fewer than all 4 subplots – the normal case for patchy invasive species. Fixed by dividing by a fixed 4 subplots instead. As part of this fix,byPlot = TRUEnow also returns aPROP_FORESTcolumn (the proportion of the plot meeting the land type/area domain, i.e.CONDPROP_UNADJ-weighted forest proportion), matching the same split already used bybiomass()’sbyPlot = TRUEoutput (BIO_ACRE/PROP_FOREST) –PROP_INV_COVERitself is not weighted by plot forest proportion.byPlot = TRUEwas the only affected output; the main population-levelCOVER_PCTwas not affected. - Fixed a bug in
invasive()wherenPlots_AREAdid not reflect restrictions imposed bylandTypeorareaDomain, instead always reporting the plot count for the broader unrestricted land base (the same class of bug already fixed intpa(),area(),carbon(),biomass(),volume(), anddwm(); see above –invasive()was the one remaining estimator missing this fix). Point estimates and sampling errors were not affected. - Fixed a bug in
invasive()where anareaDomain/landTyperestriction matching no data could produce a spurious"no non-missing arguments to max"warning instead of a clean empty result, unlike every other estimation function (which this warning class was already fixed for). Root cause: the population-estimation branch was missing a filter to drop conditions with no detected invasive species, present only in the (separate)byPlotbranch’s code; when every species group was empty, the resulting phantom “no species” row(s), rather than a genuinely empty result, bypassed the existing 0-row guard in the sharedcombineMR()utility.
biomass() and carbon()
-
biomass()no longer estimates carbon (CARB_ACRE/CARB_TOTAL/associated SE and variance columns have been removed from its output). Tree biomass estimation is otherwise unchanged. Usecarbon()for carbon stock estimation, which covers the full suite of forest ecosystem carbon pools (live and dead trees, understory vegetation, down dead wood, litter, and soil organic matter), not just standing tree carbon. - Fixed a bug in
carbon()andbiomass()wherenPlots_AREAdid not reflect restrictions imposed bylandTypeorareaDomain, instead always reporting the plot count for the broader unrestricted land base (the same class of bug already fixed intpa()andarea(); see above). Point estimates and sampling errors were not affected. Forcarbon()specifically, this also caused anareaDomainmatching no conditions to return a row ofNaNvalues instead of a clean empty result, sincecarbon()’s numerator is built by joining onto the same phantom-row-containing condition list; this is now a clean 0-row result, consistent with every other estimation function. - Fixed a bug in
biomass()wherenPlots_TREEover-counted plots for anycomponent(orbyComponent) request restricted toSTEM,STEM_BARK,STUMP_BARK,BOLE,BOLE_BARK, orBRANCH, in states with a meaningful amount of woodland-form forest (e.g. pinyon-juniper woodland in Arizona, Utah, and Colorado). NSVB does not model these components for woodland species (DRYBIO_STEM/etc. areNA, not 0, for e.g. juniper and pinyon), and while their 0 contribution toBIO_ACRE/BIO_ACRE_SEwas already handled correctly, plots whose only tallied trees were woodland species were still being counted towardnPlots_TREE. This inflatednPlots_TREEby up to ~3x in woodland-heavy states (e.g. ArizonaBRANCH: 3137 reported vs. 842 actual contributing plots); point estimates and sampling errors were not affected.
areaChange()
- Fixed a bug in
areaChange()where a condition that was nonsampled (COND_STATUS_CD == 5, e.g. hazardous or denied-access) at either measurement was misclassified as a genuine forest/non-forest (or timberland/non-timberland) land-use change event, since the sharedlandTypeDomain()helper has no distinct category for “nonsampled” – it simply isn’t forest, indistinguishable from a real non-forest reclassification. This fabricated diversion/reversion events that never actually occurred, and could biasAREA_CHNG/PERC_CHNGin either direction depending on how the affected plots happened to fall; confirmed on real data that this flipped the sign of the reported net change in forest area for Rhode Island.area()was not affected (a nonsampled condition simply contributes no area there, rather than being paired against a different point in time).
area()
- Fixed a bug in
area()where a hard-codedPLOT_STATUS_CD == 1filter (a leftover from code shared withtpa(), where it is valid since trees only occur on forest land) silently dropped every plot with no accessible forest before land-type domain indicators were applied. This caused large undercounts (up to two orders of magnitude) for everylandTypevalue other than the defaults of'forest'/'timber'(e.g.'water','non-forest','all'), and caused the documentedbyLandType = TRUEoutput to sum to well under the true total land area.landType = 'forest'/'timber'estimates were not affected. - Fixed a bug in
area()wherenPlots_AREA_DENdid not reflect restrictions imposed bylandType = 'timber'orareaDomain, instead always reporting the plot count for the broaderlandType = 'forest'land base (the same class of bug as thetpa()nPlots_AREAfix described below). Point estimates and sampling errors were not affected. - Updated
area()wherelandType = 'all'to now explicitly remove nonsampled conditions from the land area calculation (e.g. hazardous or denied-access plots). In prior versions, nonsampled conditions were included in the count oflandType = 'all', but this resulted in the sum of the different components whenbyLandType = TRUEto not sum to the total whenlandType = 'all'. - Fixed a bug where
method = 'ANNUAL'silently summed together every constituent panel’s population estimate into a single, badly inflated row (with an inflated plot count to match) instead of reporting each sampled panel-year’s own estimate separately, whenever it was run against a database that had been restricted to a single “most recent” evaluation (e.g. viaclipFIA(mostRecent = TRUE)). Confirmed on Rhode Island:area(clipFIA(fiaRI, mostRecent = TRUE), method = 'ANNUAL')previously returned a single row (labeled with the evaluation’s nominal year) that was actually the sum of all 7 constituent panels’ area estimates; it now correctly returns 7 separate rows, one per sampled panel-year, matching the values obtained by runningmethod = 'ANNUAL'against the full, unclipped inventory history. Root cause: a shared internal utility (combineMR(), used to reconcile different states’ differing “most recent” reporting years under the'TI'/'SMA'/'LMA'/'EMA'estimators) was also being unconditionally applied to'ANNUAL'output, which legitimately contains multiple distinct-year rows per state –combineMR()relabeled all of them to the same year, causing a subsequent aggregation step to sum them together. This affects every estimation function that supportsmethod = 'ANNUAL'(not justarea()), sincecombineMR()is shared internal utility; confirmed the identical corruption and fix fortpa().'TI','SMA','LMA', and'EMA'were not affected, since they already returned a single row per state before reaching this step.
volume()
- Fixed a bug in
volume()wherenPlots_AREAdid not reflect restrictions imposed bylandTypeorareaDomain, instead always reporting the plot count for the broader unrestricted land base (the same class of bug already fixed intpa(),area(),carbon(), andbiomass(); see above –volume()was the one remaining estimator missing this fix). Point estimates and sampling errors were not affected. - Fixed two related bugs in
volume()wherenPlots_TREEover-counted plots: (1) trees with no defined bole volume (e.g. dead trees under 5 inches DBH, for whichVOLCFNETis never computed) were still counted, the same class of bug just fixed inbiomass()but triggered by tree diameter rather than species; and (2) trees with a defined but exactly-zero net volume (a full cull/defect deduction can legitimately zero outVOLCFNET) were counted, where EVALIDator’s own definitions require a strictly positive volume to count a tree as contributing. Point estimates and sampling errors were not affected;nPlots_TREEwas inflated by a few percent in the states checked (e.g. Rhode IslandtreeType = 'dead': 107 reported vs. 103 actual contributing plots).
dwm()
- Fixed a bug in
dwm()wherenPlots_AREAdid not reflect restrictions imposed bylandTypeorareaDomain, instead always reporting the plot count for the broader unrestricted land base (the same class of bug already fixed intpa(),area(),carbon(),biomass(), andvolume(); see above –dwm()was the one remaining estimator missing this fix). Point estimates and sampling errors were not affected. - Fixed a bug in
dwm()whereCOND_DWM_CALCwas filtered byPLT_CNalone when restricting to the current evaluation, but a single plot can appear inCOND_DWM_CALCunder several differentEVALIDs (consecutive annual panels can each report the same not-yet-remeasured plot as their most recent down woody material data), each with slightly different evaluation/stratum-specific adjustment factors. This caused every down woody material condition to be summed once per matchingEVALIDinstead of once, inflating the reportednPlots_DWMplot count by roughly 4-5x in the states checked (e.g. Colorado: 17775 reported vs. 3897 actual contributing plots) and, more subtly, adding spurious phantom estimation-unit groups with no effect on the final point estimate or standard error (their area contribution was alwaysNAand dropped), but real effect on the plot count. Point estimates and sampling errors were not affected; onlynPlots_DWMwas. - Fixed a bug in
dwm()wherenPlots_DWMcounted every domain-qualifying, DWM-sampled plot regardless of whether it actually had any down woody material of the relevant fuel type, instead of requiring a strictly positive value (matching EVALIDator’s own per-attribute definitions, and the same class of fix just made involume()). This was checked and fixed separately for the combined default output (byFuelType = FALSE, which requires total FWD + CWD + pile volume across all fuel types to be positive, matching EVALIDator’s combined “Total volume of DWM” attribute) and forbyFuelType = TRUE’s individual fuel-type rows (each of which now requires only its own fuel type’s volume – or biomass, forDUFF/LITTER, which have no volume equivalent – to be positive, matching each fuel type’s own EVALIDator attribute). Point estimates and sampling errors were not affected. - Fixed a bug in
dwm()undermethod = 'SMA','LMA','EMA', and'ANNUAL'where the down woody material numerator was post-stratified using each plot’s original stratum assignment (fromCOND_DWM_CALC), while the area denominator used the strata produced after small strata were merged (needed for variance estimation of annual panels). Plots moved into a neighboring stratum were therefore assigned to different strata in the numerator and denominator. Under the moving-average methods, numerator rows with no matching denominator stratum/panel/group were silently dropped, so down woody material totals were underestimated, and totals acrossgrpBygroups did not sum to the ungrouped total (e.g. Colorado,areaDomain = PHYSCLCD %in% 21:29,method = 'SMA': 13.75 vs. 14.47 billion cubic feet after the fix). The numerator now uses the same (merged) strata as the denominator. Estimates withmethod = 'TI'(the default) were not affected.
tpa()
- Fixed a bug in
tpa()wherenPlots_AREAdid not reflect restrictions imposed bylandType = 'timber'orareaDomain, instead always reporting the plot count for the broaderlandType = 'forest'land base. Point estimates and sampling errors were not affected, butnPlots_AREAis documented as the recommended degrees of freedom for constructing t-based confidence intervals, so an inflated count understated the true margin of error for anylandType = 'timber'orareaDomain-restricted estimate. - Fixed a bug where a
treeDomain/areaDomainmatching no data, combined with the defaultmostRecent = TRUEbehavior, produced a spurious"no non-missing arguments to max"warning instead of a clean empty result. This affected all estimation functions (not justtpa()), since the underlying cause was in a shared internal utility. - Documented in
tpa.Rdwhy tree records with a missing diameter (DIA) – most often standing dead trees revisited after death, common in western US inventories – are excluded fromtpa()estimates regardless oftreeType: these records also lackTPA_UNADJ(FIA’s per-acre expansion factor, which itself depends onDIAto determine subplot design), so they have no valid per-acre weight under FIA’s design-based estimator and are excluded from EVALIDator’s estimates for the same reason. ConfirmedtreeType = 'dead'still matches EVALIDator to full precision in OR, where this affects a large fraction of dead-tree records (issue #32). - Fixed a bug where
method = 'EMA'silently accepted alambdaoutside its documented(0,1)range and returned degenerate weights instead of an error –NaNfor every panel at the exact boundaries (lambda = 0or1), a negative weight forlambda < 0, or an inverted (oldest-weighted-highest) recency ordering forlambda > 1.lambdais now validated up front with a clear error message. This affected every estimation function (not justtpa()), since the underlying weighting logic is a shared internal utility used by all of them, plusfsi()andcustomPSE(). - Fixed a bug where
method = 'ANNUAL'did not correctly select the best FIA evaluation to draw a panel’s standalone estimate from, when that panel is a constituent of more than one evaluation’s multi-panel window (common: e.g. panel year 2009 is part of both a 2013 evaluation’s 5-panel window and a 2014 evaluation’s window). The comparison across candidate evaluations was accidentally a no-op, so which one’s numbers ended up in the output was arbitrary (whichever came first internally) rather than the one with the most plots, as intended. This affected every estimation function usingmethod = 'ANNUAL', since the underlying selection logic is a shared internal utility.
plotFIA()
- Fixed a bug in
plotFIA()whereanimate = TRUEerrored withcould not find function "transition_manual"for any non-spatial (time-series) summary, sincetransition_manual()was called unqualified rather than asgganimate::transition_manual()–gganimateis aSuggestsdependency only, never attached bylibrary(). The analogous call in the spatial (choropleth map) branch was already correctly qualified. This is the same class of bug fixed in v1.1.1, which only fixed the spatial branch’s call. - Fixed a bug in
plotFIA()where saving an animated plot (animate = TRUEwithsavePath/fileName) always errored withcould not find function "anim_save", for the same reason as above –anim_save()is now called asgganimate::anim_save(). - Implemented the
min.yearargument, which was documented (“earliest year to be included in animation”) but had no effect on the returned plot.animate = TRUEnow drops years beforemin.yearfrom the animation; static plots are unaffected.
writeFIA()
- Fixed a bug in
writeFIA()where 16-digit CNs (e.g.,CN,PLT_CN,PREV_PLT_CN) were silently rounded to 15 significant digits on writing (e.g.,1097572188290487was written as1097572188290490), sincedata.table::fwrite()writes doubles with at most 15 significant digits. Because recent FIADB CNs have 16 digits, reading tables withreadFIA(), modifying them, and saving them withwriteFIA()broke the joins between the saved tables and any unmodified ones (e.g., COND rows no longer matched their PLOT and TREE rows), dropping recent plots from all subsequent estimates. CN columns are now written as exact integers.
Vignettes
- Added a new vignette (“Validating rFIA against EVALIDator”) that reports, for each estimation function, how many unit tests compare
rFIAestimates to EVALIDator and whether they passed, along with a history of results across test runs.
Documentation
- Updated the link to Bechtold and Patterson (2005) in all help files and the “FIA Demystified” vignette to its new location (https://research.fs.usda.gov/download/treesearch/20371.pdf).
rFIA v1.1.4
CRAN release: 2026-06-26
- Removed
.dotsargument from all calls todplyr::group_by(), which resulted in an error with the latest version ofdplyr(see #54). - Removed dependency on the
bit64package. - Removed
Nfrom the return output of all model fitting functions as this was not always being properly calculated when different filters were applied. Additionally, we updated our recommended approach for calculating confidence intervals and so this value is no longer part of that recommended calculation. - Removed the argument
variancefrom all estimation functions, with the exception offsi(). Previous documentation was misleading in that it said valid confidence intervals cannot be constructed from the sampling errors. This is not strictly true. The sampling error is a function of the variance/standard error, and so the sampling error can be used to calcualte confidence intervals when manipulated appropriately. See the note in all model fitting functions documentation for further details on how to do this. - Confidence interval calculations provided by
fsi()were too precise. This has been updated to better reflect the amount of uncertainty in the associated estimates. Confidence intervals are now calculated with the number of plots used to inform the given FSI estimate, not the number of plots within all estimation units that encompass the population of interest. Consider the case where we calculate FSI for an individual species. Because FSI is a measure of change over time, only plots where the species was present at for at least one time point go into informing the FSI estimate. The previous use of all plots, even those without the species of interest, substantially inflated the sample size. - Fixed some minor bugs in
plotFIA()that errored when including error bars. Also fixed code to remove a warning message in simple time series plots. - Updated the “Estimating Forest Attributes” document, particularly the section on sampling error and how to calculate confidence intervals.
rFIA v1.1.3
CRAN release: 2026-04-30
- Substantial updates to the
fsi()function. Some of these functions fixed some common errors that could be encountered under specific circumstances where the function broke, which happened as a result of updates to FIADB since the last time this function underwent a major update. An additional update fixes an important bug where thescaleByfunction would not always work as was reported. In particular, under certain situations (namelybyPlot = TRUE) the subsequent calculations of relative density did not use the level-specific intercepts and slopes that were estimated in the regression model, and instead the overall mean was used (i.e., equivalent to ifscaleBywas not specified). This could lead to sub-optimal accuracy of the relative density calculations, and in subsequent FSI outputs. Apologies for any problems this may have caused. - Updated the
biomass()function to fix a bug in reported estimates whencomponent = 'TOTAL'. There was a mismatch in what was reported between the documentation and the function output. The estimate provided simply added up all biomass across the different components provided bybiomass(), which did not make much sense since the different components are not mutually exclusive. This is now fixed such thatcomponent = 'TOTAL'provides biomass estimates equal to the sum of ROOT, STEM, STEM_BARK, BRANCH, and FOLIAGE components. Apologies for the inconvenience this error may have caused.
rFIA v1.1.2
CRAN release: 2025-09-29
- Updated all estimation functions to allow grouping by variables in the
PLOTGEOMdatabase table within thegrpByargument. Also changedreadFIA()to by default read inPLOTGEOMas one of the common database tables. Thanks to Jacob Fraser for the suggestion here. -
Fixed a bug with
dtplyr 1.3.2that led to an error inareaChange()
rFIA v1.1.1
CRAN release: 2025-03-10
- Jeff Doser is the new package maintainer. Please send all inquiries via email to Jeff (jwdoser@ncsu.edu) or post potential bugs on the GitHub development page.
- Updated the
fiaRIobject to reflect recent changes in the FIA Database. These changes resulted in the package functions successfully working with the previous version offiaRIbut not working for actual user data when pulling data from recent versions of the FIA Database. - Updated functionality for working with external spatial (
sf) objects with the following functions:tpa(). Changes in recent versions of thesfpackage led to errors when attempting to return a spatial object. This bug is now fixed. - Updated a substantial bug in
area()andareaChange()that resulted in incorrect area (or area change) estimates being reported when specifyingtreeDomainandgrpBy(when using grouping variables from TREE). In the previous version, the filters were not properly applied, and so area estimates did not adequately represent the filtering conditions and often just provided the same values as iftreeDomainwas not specified. Estimates now provide correct results that are more inline with intuition. For example, if specifyingtreeDomain = SPCD == 121[i.e., longleaf pine], the previousarea()function would essentially ignore this and return area of all forest plots. Now,area()will return the estimate of land area where at least one longleaf pine tree occurs. Further, the estimate of percent area will be the percentage of total land area (which is determined bylandType) that contains longleaf pine. - Substantial updates to
biomass(). Previous versions were not compatible with updates in FIADB and the new National Scale Volume and Biomass (NSVB) estimators. The function is now updated and returns biomass and carbon estimates using the NSVB procedure. - Updated
findEVALID()to return the correct evaluation IDs. Previous versions had an incorrect join that resulted in additional, incorrect EVALIDs being returned for a given set of criteria. This function should only be used by users familiar with FIA and desiring to use FIA data for use outside ofrFIA, asrFIAis built in a way that users do not need to directly interact with EVALIDs. - Updated
dwm()whenbyPlot = TRUEto set theYEARcolumn equal to the year each plot was measured (MEASYEAR), which may differ slightly from its associated inventory year (INVYR). This is what all otherrFIAfunctions do and what was reported in the manual, but theYEARreturned prior to this version was actually the inventory year. - Fixed a bug with
growMort()that resulted in estimates of mean annual survivor growth and mean annual net change reporting as 0. - Fixed a discrepancy with
growMort()calculation of removals and the description of it in the manual. Removal estimates provided bygrowMort()do NOT include stems that grow beyond the 5-inch diameter threshold and then are subject to harvest or natural mortality before the remeasurement period. In other words,rFIArecruitment does not include trees corresponding to FIA growth components of CUT2 and MORTALITY2. - Fixed a typo in the
standStruct()documentation that incorrectly said the lower diameter for Pole class was set at 11cm while it is in fact set at 12.7cm (5in). - Fixed typo in documentation of
plotFIA()regarding the error bars produced whense = TRUE. These are 95% confidence intervals, not 68% confidence intervals. - Added more details to
vegStruct()on reporting of estimates by canopy layer and growth habit. - Updated internal data to now contain the Dec 2024
REF_SPECIEStable from FIADB, which provides access to theCARBON_RATIO_LIVEattribute for using the NSVB species-specific carbon fractions. - Updated all estimation functions to fix a bug that resulted in an error when setting
method = 'EMA'. - Removed all references to “ECOSUBCD” in the help pages since this column was removed from the PLOT table in FIADB v9.3.
- Updated
writeFIA()to allow users to write database tables by state when only a subset of the table is originally read into R. This currently requires either the PLOT or COND tables to be read in. - Fixed a bug in
plotFIA()that led to an error in animated plots whengganimatewas not loaded (note thatgganimatestill needs to be installed).