Chapter 2: Forest Inventory and Data Preparation
Learning Objectives
After reading this chapter, you should be able to:
Describe the structure of forest inventory data and how it maps to ws3 development types
Prepare raw inventory data for use with ws3
Aggregate inventory data into development types
Validate inventory data for consistency and completeness
What Is Forest Inventory Data?
Forest inventory data is the foundation of any wood supply model. It describes the current state of the forest: what grows where, how much of it there is, and what condition it’s in.
Inventory data typically comes from:
Field surveys: Sample plots measured by foresters
Remote sensing: LiDAR, aerial photography, satellite imagery
Management records: Past harvest plans, silviculture treatments
Provincial databases: Government forest inventory databases
In British Columbia, the primary source is the Forest Inventory Database (FIB) maintained by the Ministry of Forests. This database contains hundreds of thousands of inventory records covering the entire province.
Inventory Data Structure
A typical inventory dataset has the following columns:
Column |
Type |
Description |
|---|---|---|
|
string |
Unique identifier for the sample plot |
|
string |
Dominant tree species (e.g., “Douglas-fir”, “Western red cedar”) |
|
float |
Site productivity index (height at reference age) |
|
int |
Stand age in years |
|
float |
Dominant height in meters |
|
float |
Basal area in m²/ha |
|
float |
Merchantable volume in m³/ha |
|
float |
Stems per hectare |
|
float |
Canopy closure (0-100%) |
|
geometry |
Spatial location (point, polygon) |
Preparing Data for ws3
ws3 expects inventory data in a specific format. The ws3.forest.ForestModel
class can accept pandas DataFrames directly.
Step 1: Load Your Data
import pandas as pd
# Load inventory data from CSV
inventory = pd.read_csv("forest_inventory.csv")
# Or load from a GeoJSON file
inventory = pd.read_json("forest_inventory.geojson")
Step 2: Clean the Data
# Remove rows with missing critical fields
inventory = inventory.dropna(subset=["species", "age", "volume"])
# Standardize species names
species_map = {
"Douglas-fir": "Pseudotsuga menziesii",
"DF": "Pseudotsuga menziesii",
"Western red cedar": "Thuja plicata",
"Cedar": "Thuja plicata",
"Sitka spruce": "Picea sitchensis",
"Spruce": "Picea sitchensis"
}
inventory["species"] = inventory["species"].map(species_map)
Step 3: Define Development Types
# Create development type codes
inventory["dt_code"] = (
inventory["species"] + "-" +
inventory["site_index"].astype(int).astype(str)
)
# Aggregate by development type
dt_summary = inventory.groupby("dt_code").agg(
area=("plot_id", "count"),
mean_age=("age", "mean"),
mean_volume=("volume", "mean"),
mean_site_index=("site_index", "mean")
).reset_index()
print(dt_summary)
Step 4: Create the Forest Model
from ws3.forest import ForestModel
# Create the model with required parameters
fm = ForestModel(
model_name="inventory_model",
model_path=".",
base_year=2024,
horizon=20,
period_length=10,
max_age=200
)
# Development types are loaded from Woodstock-format data files,
# not constructed individually. The typical workflow is:
#
# fm.import_areas_section("areas.txt")
# fm.import_yields_section("yields.txt")
# fm.import_actions_section("actions.txt")
# fm.import_transitions_section("transitions.txt")
#
# For programmatic construction from a pandas DataFrame, you would
# iterate over your aggregated development types and populate the
# model's internal data structures accordingly. See the ws3 source
# for the import methods above.
# Access development types via the dtypes dict
print(f"Development types: {len(fm.dtypes)}")
# Calculate total area across all periods and development types
total_area = 0.0
for period in fm.periods:
for dtype in fm.dtypes.values():
total_area += dtype.area(period)
print(f"Total area: {total_area:.1f} hectares")
Data Validation
Before running simulations, validate your inventory data:
# Check for negative areas
assert (inventory["area"] >= 0).all(), "Negative areas found"
# Check for reasonable ages
assert (inventory["age"] >= 0).all(), "Negative ages found"
assert (inventory["age"] <= 500).all(), "Unreasonably high ages found"
# Check for consistent species names
assert inventory["species"].notna().all(), "Missing species names"
# Check that total area is reasonable
total_area = inventory["area"].sum()
print(f"Total inventory area: {total_area:.1f} hectares")
Common Data Issues
Missing species names: Replace with “Unknown” or exclude from model
Negative values: Check for data entry errors
Inconsistent units: Ensure all volumes are in m³/ha, all ages in years
Duplicate records: Check for duplicate plot IDs
Outlier values: Check for unreasonable site indices or volumes
Exercises
Exercise 1 (Easy): Load a sample inventory dataset and create a ws3 model. Print the development type summary.
Exercise 2 (Medium): Write a function that takes a pandas DataFrame and returns a list of development types suitable for adding to a ForestModel.
Exercise 3 (Hard): Extend the data cleaning function to handle missing site indices by estimating them from dominant height and age using a site index curve.
Further Reading
Chapter 1: Forest Estate Models — Forest estate model fundamentals
Chapter 3: Growth and Yield — Growth curve fitting and interpolation
Loading a Woodstock Model — Detailed data preparation guide
Data Contracts — Data contracts and module boundaries