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).
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.
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).
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.87The 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.
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):
standard (default) uses the measured
increment wherever a tree was identified and is present in the second
survey. A tree present only in the first survey (harvested or dead)
receives a forward estimate over half the period; a tree present only in
the second survey that could not be identified receives a backward
estimate over the whole period.min_estimates uses an estimate only
where a tree cannot be identified across the surveys; a tree present
only in the first survey then receives a zero increment.all_estimates replaces every measured
increment with a model estimate.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.
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.
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.57The 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.29Both 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:
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.33Yield 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>