Estimating Forest Volume Increment With FeNEU

library(FeNEU)
library(dplyr)

options(fe_spec_lang = "eng") # display species names in English

1 Introduction

Volume increment cannot be read off an inventory the way standing volume can. FeNEU estimates it in three ways, and the main vignette gives the outside view – what each looks like from the calling side. This vignette is about the methods themselves.

Approach Data required Reports
Repeated survey two surveys of the same plots any tree collective, with a confidence interval
BWI 3 growth functions one survey, with tree ages any tree collective
Yield tables one survey, with tree ages main stand only

The repeated survey observes the increment from measurements taken twice; the other two model it from a single survey. One consequence runs through everything below: Only the repeated survey carries a confidence interval – the two single-inventory methods are fully model-based and report none.

All three build extensively on ForestElementsR (Biber and Toraño Caicoya 2026), and all three return an object of class fe_increment – a bundle that carries the estimate together with the figures its reports print – so from the estimate onward they are consumed the same way (Section 6).

2 Preparing the tree data

Each entry point takes two things: the fe_inventory object – two of them, for a repeated survey – and a prepared tree list pulled from it. The inventory carries the plot structure the estimate needs (plot matching, represented areas, survey years, the figures for the report header); the tree list carries the completed heights and the derived tree attributes (species group, basal area, diameter and height classes). Pulling and preparing the tree list is time-consuming, so it is done once and passed alongside the inventory.

The canonical preparation is

twh <- inv |> pull_trees() |> height_complete_inventory()
inv |> fill_heights_back(twh) |> pull_trees() |> trees_add_essentials(method = "BaySF")

The second pull_trees() is the point: fill_heights_back() writes the estimated heights into the inventory, so the re-pull carries them in height_m and no working column travels on. The tree list is what you keep; the inventory you pass to the estimate is the original object – the estimate takes the tree heights from the list, and uses the inventory only for the plot geometry it needs to match plots and trees and to weight by represented area.

The package ships the prepared lists of the ex3 survey pair, so we start from them.

inv_1st   <- data_ex3_previous_sample_fe_inventory
inv_2nd   <- data_ex3_sample_fe_inventory
trees_1st <- data_ex3_previous_sample_trees_essentials
trees_2nd <- data_ex3_sample_trees_essentials

3 Increment from a repeated survey

Two surveys of the same plots let the increment be observed rather than modelled. inv_increment_repeated_survey() covers the whole chain – the plots of the two surveys are matched, the trees within them are matched, an increment is derived for every tree, and the trees that could not be matched are dealt with according to the fill_option – and returns the bundle:

inc <- inv_increment_repeated_survey(
  inv_1st       = inv_1st,
  inv_2nd       = inv_2nd,
  inv_1st_trees = trees_1st,
  inv_2nd_trees = trees_2nd,
  method        = "rep_classic",
  fill_option   = "standard",
  progress_bar  = FALSE
)

inc
#> <fe_increment_repsurv>
#>   Surveys      : 2009 -> 2020  (period 11 yr)
#>   Area / plots : 46.7 ha / 10
#>   Method       : rep_classic / fill: standard
#>   Trees        : 139
#>   Increment    : 9.57 m3/ha/yr (summaric balance)
#>   Steps kept   : no
#>   Use increment_base_table() / output_increment_overall() on this object.
names(inc)
#> [1] "trees"    "overview" "summaric" "meta"     "steps"

trees holds the per-tree increments, overview the aggregation by plot and enterprise, summaric the increment balance of the enterprise, and meta the figures the reports print in their header. steps holds the intermediate results of the chain and is NULL unless keep_steps = TRUE is requested.

Two arguments decide what the estimate means: method (Section 3.1) and fill_option (Section 3.2).

3.1 The four methods

method selects how a tree’s increment is derived from its two measurements. The trees differ in whether they crossed a concentric-circle threshold between the surveys – and thus changed the number of trees they represent per hectare (\(n_1 \neq n_2\)) – which is exactly what the methods handle differently. Write \(v_1, v_2\) for the tree’s volume and \(n_1, n_2\) for its representation number at the first and second survey.

rep_classic is the default and, with rep_trans, the one to reach for first. A re-measured tree contributes \[iv = n_2 v_2 - n_1 v_1,\] an ingrowth tree its full second volume \(iv = n_2 v_2\). This reproduces the classic stand-level increment (the German ertragsgeschichtlicher Zuwachs), which is its virtue: the enterprise total agrees with the balance-based figure (Section 3.4). Its price is that a tree which crossed a threshold (\(n_2 < n_1\)) can come out with a negative increment when the volume gain does not outweigh the drop in representation number – odd at first sight, but statistically valid.

rep_trans avoids that by splitting a threshold-crossing tree at the threshold. A tree that did not cross contributes \(iv = n\,(v_2 - v_1)\); one that crossed contributes \[iv = n_2 (v_2 - v_t) + n_1 (v_t - v_1),\] where \(v_t\) is its volume at the threshold. It always yields \(iv \geq 0\), at the cost of no longer matching the classic stand-level figure.

The remaining two are simpler variants for re-measured trees, both guaranteeing \(iv \geq 0\) (with clamp_plausible_shrinkage = TRUE) and neither matching the classic figure: rep_mean weights the volume change with the mean representation number, \(iv = \tfrac{n_1 + n_2}{2}(v_2 - v_1)\), and rep_end with the second, \(iv = n_2 (v_2 - v_1)\).

The choice moves the enterprise increment noticeably (however significantly more for our extremely small example (10 inventory points) than it would for a typical sample inventory with several hundred up to several thousand points):

overall <- function(inc_m, m) {
  tc  <- output_increment_overall(inc_m)$table_combined
  tot <- tc[tc$is_total_row, ]
  tibble(method = m, iv_m3_ha_yr = tot$iv_m3_ha_yr_st)
}

# `inc` above is already the rep_classic run, so only rep_trans is new here
inc_trans <- inv_increment_repeated_survey(
  inv_1st, inv_2nd, trees_1st, trees_2nd,
  method = "rep_trans", fill_option = "standard", progress_bar = FALSE
)

bind_rows(overall(inc, "rep_classic"), overall(inc_trans, "rep_trans"))
#> # A tibble: 2 × 2
#>   method      iv_m3_ha_yr
#>   <chr>             <dbl>
#> 1 rep_classic        9.35
#> 2 rep_trans          7.87

The figure compared here is the per-tree route (iv_m3_ha_yr_st), the one that responds to the method; the enterprise balance – the number the bundle print reports – does not, and Section 3.4 explains the difference.

3.2 Filling the gaps

Not every tree can be matched. fill_option governs what happens to those that cannot, using backward and forward estimates from the BWI 3 growth functions (Section 4):

Plots that exist only in the second survey always carry estimates, whatever the option says – there is no earlier measurement that an estimate could displace.

3.3 The individual steps

inv_increment_repeated_survey() runs a chain of six functions, each exported so a single stage can be inspected or varied. Ordinary work needs only the one call above; the steps, in order, are inv_increment_repsurv_ccirc() (plot and tree matching, the four methods), inv_inc_fill_gaps_gnfi3() (the BWI 3 estimates for the gaps), inv_inc_tree_consolidate() (apply the fill_option), inv_inc_tree_extend() (add the age and diameter classes), inv_inc_tree_extend_combined() (fold in the second-only plots), and inv_inc_big_overview() (the enterprise level). See their help pages for the details of each.

3.4 Reports

From the bundle there are two directions. Broken down by species group and class, in the layout of the status-quo base tables (see the main vignette):

increment_base_table_main_stand(inc, by_class = "age") |>
  output_increment_base_table() |>
  head(10)
#> # A tibble: 10 × 12
#>    species_group variable      `(0,20]` `(20,40]` `(40,60]` `(60,80]` `(80,100]`
#>    <bav_st_shrt> <chr>            <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
#>  1 spruce        n_plot_class        NA     1         2         1           3   
#>  2 spruce        n_iv_total          NA     7        14         1          21   
#>  3 spruce        n_iv_filled         NA     7        14         0          10   
#>  4 spruce        iv_m3_yr_tot…       NA    12.0      34.4       5.77       64.7 
#>  5 spruce        ci95_iv_m3_y…       NA    25.7      69.3      13.2        92.0 
#>  6 spruce        iv_m3_ha_yr         NA     0.256     0.737     0.124       1.39
#>  7 spruce        ci95_iv_m3_h…       NA     0.549     1.48      0.282       1.97
#>  8 spruce        area_ha             NA     6.05      3.48      0.198       5.78
#>  9 spruce        iv_m3_ha_yr_…       NA     1.98      9.89     29.2        11.2 
#> 10 pine          n_plot_class        NA    NA        NA         2           3   
#> # ℹ 5 more variables: `(100,120]` <dbl>, `(120,140]` <dbl>, `(140,160]` <dbl>,
#> #   `(160,999]` <dbl>, total <dbl>

by_class = "dq" groups by quadratic-mean-diameter class instead, for inventories without ages. The other direction is the enterprise level:

ovr <- output_increment_overall(inc)

ovr$table_combined |>
  select(species_group, is_total_row, iv_m3_ha_yr_st, iv_m3_ha_yr_sum)
#> # A tibble: 8 × 4
#>   species_group  is_total_row iv_m3_ha_yr_st iv_m3_ha_yr_sum
#>   <bav_st_shrt>  <lgl>                 <dbl>           <dbl>
#> 1 spruce         FALSE                3.94            4.15  
#> 2 pine           FALSE                4.81            4.80  
#> 3 larch          FALSE                0.139           0.130 
#> 4 Douglas fir    FALSE                0.168           0.168 
#> 5 beech          FALSE                0.115           0.115 
#> 6 oak            FALSE                0.0476          0.0476
#> 7 other hardwood FALSE                0.503           0.532 
#> 8 NA             TRUE                 9.35            9.57

The total row is the enterprise as a whole. The two columns are two routes to it: _st adds up the individual-tree increments, _sum is the balance of the two surveys over the whole measured population (second-survey volume, minus first, plus what was removed in between). For the enterprise the balance is the reference figure; the per-tree route is what allows the breakdown into species and classes. Besides table_combined (all points), the result holds table_matched (points measured twice) and table_2nd_only (points the later survey added). The confidence interval lives in table_matched – it is derived from the scatter between the points measured twice, which the second-only points cannot contribute to, so the combined table carries none:

ovr$table_matched |>
  filter(is_total_row) |>
  select(iv_m3_ha_yr_st, ci95_iv_m3_ha_yr_st)
#> # A tibble: 1 × 2
#>   iv_m3_ha_yr_st ci95_iv_m3_ha_yr_st
#>            <dbl>               <dbl>
#> 1           9.80                2.29

Both directions have a *_pdf() counterpart – output_increment_base_table_pdf() and output_increment_overall_pdf() – following the three-step pattern (aggregate, restructure, render) of the status-quo tables:

increment_base_table_main_stand(inc, by_class = "age") |>
  output_increment_base_table() |>
  output_increment_base_table_pdf(
    tab_title  = "Example forest enterprise",
    type       = "age",
    output_dir = tempdir()
  )

output_increment_overall_pdf(ovr, output_dir = tempdir())

4 Increment from a single survey with BWI 3 functions

Where only one survey exists, the increment can be estimated from the current state of the trees. The growth functions of the third German National Forest Inventory (BWI 3) (Riedel et al. 2017) estimate each tree’s diameter and height growth from its own dimensions and age; the volume increment follows from the projected volumes. inv_increment_gnfi3() covers the chain and returns the same kind of bundle:

inc_bwi3 <- inv_increment_gnfi3(
  inv       = inv_2nd,
  inv_trees = trees_2nd,
  dt        = 5
)

inc_bwi3
#> <fe_increment_gnfi3>
#>   Survey       : 2020 
#>   Area / plots : 46.7 ha / 10
#>   Method       : BWI 3 growth functions, single inventory
#>   Trees        : 108 
#>   Increment    : 7.33 m3/ha/yr
#>   Steps kept   : no 
#>   Use increment_base_table() on this object.

dt is the span the estimate covers, in years: a positive value looks forward, a negative one back. Five years is a deliberate default – the further such an estimate reaches, the more it is affected by removals, mortality and ingrowth it cannot know about. Because the estimate ages every tree, it needs the age of every tree; where a stand layer carries none the function stops rather than evaluating only the part that happens to have one. The estimate is made for the whole population per tree, so any collective can be reported from it; it carries no confidence interval.

From here the reports of Section 3.4 apply unchanged – the class tables, and a species-group overview of the annual increment:

increment_base_table(inc_bwi3, by_class = "age") |>
  output_increment_overview_gnfi3()
#> # A tibble: 9 × 4
#>   species_group  is_total_row zv_m3_yr zv_m3_ha_yr
#>   <bav_st_shrt>  <lgl>           <dbl>       <dbl>
#> 1 spruce         FALSE          163.        3.49  
#> 2 pine           FALSE          118.        2.53  
#> 3 larch          FALSE            7.35      0.157 
#> 4 Douglas fir    FALSE            5.91      0.126 
#> 5 beech          FALSE            3.88      0.0829
#> 6 oak            FALSE            6.92      0.148 
#> 7 other hardwood FALSE           34.0       0.728 
#> 8 noble hardwood FALSE            3.46      0.0741
#> 9 NA             TRUE           343.        7.33

5 Increment from a single survey with yield tables

Yield tables take the other route. They work at stand level rather than per tree, which requires the area to be split into the shares of virtual monospecific stands, so this approach applies to the main stand only. FeNEU uses the yield-table system of ForestElementsR (vignette("yield_tables", package = "ForestElementsR")).

inc_yt <- inv_increment_ytables(
  inv              = inv_2nd,
  inv_trees        = trees_2nd,
  ytable_selection = ytables_bavrn_state_var_1_feneu
)

inc_yt
#> <fe_increment_ytables>
#>   Survey       : 2020 
#>   Area / plots : 46.7 ha / 10
#>   Method       : yield tables, single inventory
#>   Collective   : Hauptbestand 
#>   Cohorts      : 22 (plot x species)
#>   Increment    : 4.19 m3/ha/yr
#>   Use increment_ytables_base_table() on this object.

ytables_bavrn_state_var_1_feneu assigns a yield table to each species code. It is the set FeNEU ships. The assignment can be replaced by one of your own.

The estimate is made for each cohort – one species in the main stand of one inventory plot, on the virtual monospecific area it occupies there. The cohort’s age and mean height give its site index; age and site index give the per-hectare increment the table holds; and that figure is corrected by the stocking level, the cohort’s basal area over the basal area the table expects. A cohort has one age and one mean diameter, so grouping a report by age class or by diameter class labels the same estimate rather than changing it. There is no confidence interval.

inc_yt$overview
#> # A tibble: 9 × 7
#>   species_group  area_ha site_index zv_m3_ha_yr zv_m3_yr is_total_row
#>   <bav_st_shrt>    <dbl>      <dbl>       <dbl>    <dbl> <lgl>       
#> 1 spruce          15.3        0.895        6.58   101.   FALSE       
#> 2 pine            21.6        0.729        3.12    67.2  FALSE       
#> 3 Douglas fir      0.687      1.36         2.70     1.85 FALSE       
#> 4 beech            0.812      2.48         3.61     2.93 FALSE       
#> 5 oak              0.610      3            4.99     3.04 FALSE       
#> 6 other hardwood   4.24       1.30         3.84    16.3  FALSE       
#> 7 noble hardwood   0.563     -0.531        6.78     3.81 FALSE       
#> 8 NA               2.97      NA           NA       NA    FALSE       
#> 9 NA              46.7       NA            4.19   196.   TRUE        
#> # ℹ 1 more variable: is_no_main_stand_row <lgl>

area_ha is the virtual area of each species group, site_index its mean yield class, and zv_m3_ha_yr the increment on that virtual area. The total row relates the whole increment to the entire inventory area, the larger reference, and its site_index is empty because the index scales of different tables cannot be averaged.

Yield-table increments typically run lower than the other two methods, and this is expected: the tables in use describe a growth level that the observed increment in Central Europe has exceeded over recent decades (Pretzsch et al. 2014, 2023). The value is still a well-established, easy-to-communicate baseline. The class tables are built and formatted exactly as for the other methods:

increment_ytables_base_table(inc_yt, by_class = "age") |>
  output_increment_base_table() |>
  head(10)
#> # A tibble: 10 × 12
#>    species_group variable      `(0,20]` `(20,40]` `(40,60]` `(60,80]` `(80,100]`
#>    <bav_st_shrt> <chr>            <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
#>  1 spruce        n_plot_class        NA      1        1        1           3    
#>  2 spruce        site_index          NA      1.35    -0.321    2.23        0.473
#>  3 spruce        iv_m3_yr_tot…       NA     49.1      5.74     1.49       28.6  
#>  4 spruce        ci95_iv_m3_y…       NA     NA       NA       NA          NA    
#>  5 spruce        iv_m3_ha_yr         NA      1.05     0.123    0.0318      0.611
#>  6 spruce        ci95_iv_m3_h…       NA     NA       NA       NA          NA    
#>  7 spruce        area_ha             NA      6.05     0.509    0.257       5.77 
#>  8 spruce        iv_m3_ha_yr_…       NA      8.12    11.3      5.79        4.94 
#>  9 pine          n_plot_class        NA     NA       NA        2           3    
#> 10 pine          site_index          NA     NA       NA        0.608       0.894
#> # ℹ 5 more variables: `(100,120]` <dbl>, `(120,140]` <dbl>, `(140,160]` <dbl>,
#> #   `(160,999]` <dbl>, total <dbl>

References

Biber, Peter, and Astor Toraño Caicoya. 2026. ForestElementsR: Data Structures and Functions for Working with Forest Data. https://doi.org/10.32614/CRAN.package.ForestElementsR.
Pretzsch, Hans, Peter Biber, Gerhard Schütze, Enno Uhl, and Thomas Rötzer. 2014. “Forest Stand Growth Dynamics in Central Europe Have Accelerated Since 1870.” Nature Communications 5 (September). https://doi.org/10.1038/ncomms5967.
Pretzsch, Hans, Miren del Río, Catia Arcangeli, et al. 2023. “Forest Growth in Europe Shows Diverging Large Regional Trends.” Scientific Reports 13 (1): 15373. https://doi.org/10.1038/s41598-023-41077-6.
Riedel, T., P. Hennig, F. Kroiher, H. Polley, F. Schmitz, and Schwitzgebel F. 2017. Die Dritte Bundeswaldinventur (BWI 2012). Inventur- Und Auswertungsmethoden. Thuenen Institut fuer Waldoekosysteme.