Skip to main content
NSF NEON, Operated by Battelle

Main navigation

  • About
    • NEON Overview
      • Vision and Management
      • Spatial and Temporal Design
      • History
    • About the NEON Biorepository
      • ASU NEON Biorepository Staff
      • Contact the NEON Biorepository
    • Observatory Blog
    • Newsletters
    • Staff
    • FAQ
    • User Accounts
    • Contact Us

    About

  • Data
    • Data Portal
      • Data Availability Charts
      • API & GraphQL
      • Prototype Data
      • Externally Hosted Data
    • Data Collection Methods
      • Airborne Observation Platform (AOP)
      • Instrument System (IS)
        • Instrumented Collection Types
        • Aquatic Instrument System (AIS)
        • Terrestrial Instrument System (TIS)
      • Observational System (OS)
        • Observation Types
        • Observational Sampling Design
        • Sampling Schedules
        • Taxonomic Lists Used by Field Staff
        • Optimizing the Observational Sampling Designs
      • Protocols & Standardized Methods
    • Getting Started with NEON Data
      • neonUtilities for R and Python
      • Learning Hub
      • Code Hub
    • Using Data
      • Data Formats and Conventions
      • Released, Provisional, and Revised Data
      • Data Product Bundles
      • Usage Policies
      • Acknowledging and Citing NEON
      • Publishing Research Outputs
    • Data Notifications
    • NEON Data Management
      • Data Availability
      • Data Processing
      • Data Quality

    Data

  • Samples & Specimens
    • NEON Biorepository Sample Portal at ASU
    • About Samples
      • Sample Types
      • Sample Repositories
      • Megapit and Distributed Initial Characterization Soil Archives
    • Finding and Accessing Sample Data
      • Species Checklists
      • Sample Explorer - Relationships and Data
      • Biorepository API
    • Requesting and Using Samples
      • Requesting Samples from the NEON Biorepository
      • Request Megapit and Initial Characterization Soil
      • Sample Use Guidelines
      • Sample Use Policy
      • Acknowledging and Citing the NEON Biorepository

    Samples & Specimens

  • Field Sites
    • Field Site Map and Info
    • Spatial Data Layers & Maps

    Field Sites

  • Resources
    • Getting Started with NEON Data
    • Research Support Services
      • Field Site Coordination
      • Letters of Support
      • Permits and Permissions
      • AOP Flight Campaigns
      • Research Support FAQs
      • Research Support Projects
    • Code Hub
      • neonUtilities for R and Python
      • Code Resources Guidelines
      • Code Resources Submission
      • NEON's GitHub Organization Homepage
    • Learning Hub
      • Tutorials
      • Workshops & Courses
      • Science Videos
      • Teaching Modules
    • Science Seminars and Data Skills Webinars
    • Document Library
    • Funding Opportunities

    Resources

  • Impact
    • Research Highlights
    • Papers & Publications
    • NEON in the News

    Impact

  • Get Involved
    • Upcoming Events
    • Past Events
    • Research and Collaborations
      • Environmental Data Science Innovation and Inclusion Lab
      • Collaboration with DOE BER User Facilities and Programs
      • EFI-NEON Ecological Forecasting Challenge
      • NEON Great Lakes User Group
      • NCAR-NEON-Community Collaborations
    • Advisory Groups
      • Science, Technology & Education Advisory Committee (STEAC)
      • Innovation Advisory Committee (IAC)
      • Technical Working Groups (TWG)
    • NEON Ambassador Program
      • Exploring NEON-Derived Data Products Workshop Series
    • Partnerships
    • Community Engagement
    • Work Opportunities

    Get Involved

  • My Account
  • Search

Search

Learning Hub

  • Tutorials
  • Workshops & Courses
  • Science Videos
  • Teaching Modules

Breadcrumb

  1. Resources
  2. Learning Hub
  3. Tutorials
  4. Exploring diel carbon flux cycles

Tutorial

Exploring diel carbon flux cycles

Authors: David Durden

Last Updated: Jun 30, 2026

This data tutorial provides an overview of exploring NEON carbon flux data, using the neonUtilities R package. If you are just starting to work with NEON data it is recommended that you also checkout the more general tutorials, such as the Download and Explore NEON Data tutorial or the Introduction to working with NEON eddy flux data. This tutorial builds off the data exploration techniques described in the aforementioned tutorials, and will explore carbon dynamics at two forested ecosystem sites in NEON's Domain 5 - Great Lakes, STEI and TREE. The STEI and TREE sites are only located approximately 1 mile from each other, but experience very different land management practices.

This tutorial assumes general familiarity with eddy-covariance data, and will briefly discuss both turbulent (turb) and storage (stor) fluxes in relation to the net surface atmosphere exchange (nsae). The nsae for CO2 fluxes is often referred to as Net Ecosystem Exchange (NEE).

1. Set up environment and download data

Start by installing and loading packages and setting options. The rhdf5 package is hosted on Bioconductor, requiring BiocManager to be installed to allow rhdf5 to be pulled from the Bioconductor repository. You can skip the installation step if these packages are already installed on your machine, although checking for updates is always wise.

## 

## install.packages('BiocManager')

## BiocManager::install('rhdf5')

## install.packages('neonUtilities')

## install.packages('ggplot2')

## install.packages('dplyr')

## install.packages('tidyr')

## 

library(neonUtilities)

library(rhdf5)

library(ggplot2)

library(dplyr)

library(tidyr)

## 

Now that all the required packages are loaded, let's define all the necessary function arguments to download and stack our flux data using the neonUtilities package's zipsByProduct() and stackEddy() functions. I will define the input arguments as variables to make it easy to modify if I want to explore additional sites or months of data. For this analysis, we will focus on the 2022 growing season at STEI and TREE. Please be aware that the data download size is ~ 1 GB. The code can be modified to shorten or extend this window, but to get a good look at the carbon dynamics at the site it was important to capture most of the growing season.

Inputs to the zipsByProduct() function:

  • dpID: DP4.00200.001, the bundled eddy covariance product
  • package: basic (the expanded package has additional quality metrics and advanced footprint matrices that are not relevant to this tutorial)
  • site: STEI and TREE
  • startate: 2022-04
  • enddate: 2022-09
  • savepath: modify if desired; code below sets to the working directory
  • check.size: TRUE if you want to see file size before downloading, otherwise FALSE
  • token: NEON API token. Required for data downloads. Tokens can be generated in NEON data portal user accounts - log in to your account or create one, and go to the API Tokens section. For best practices in storing and using tokens, follow the instructions here.

The download may take a while, especially if you're on a slow network. For faster downloads, consider using an API token.

token <- Sys.getenv("NEON_TOKEN")



startDate <- "2022-04"

endDate <- "2022-09"



site <- c("STEI", "TREE")



dirFile <- getwd()



zipsByProduct(dpID="DP4.00200.001", package="basic", 
              site=site, 
              startdate=startDate, 
              enddate=endDate,
              release="RELEASE-2024",
              savepath=dirFile, 
              check.size=FALSE,
              token=token)

2. Stacking Level 4 Flux Data

There are five levels of data contained in the eddy flux bundle. For full details, refer to the NEON algorithm document.

In this tutorial we will only be focusing on Level 4 (dp04) flux data products; however, additional data products from Level 0' (dp0p) calibrated raw data to Level 3 (dp03) spatially interpolated vertical profiles used to derive the storage flux are available in the EC bundled HDF5 files. Information can be found on the <a href="NEON algorithm document" target="_blank">NEON Bundled data product - eddy covariance webpage. The Introduction to working with NEON eddy flux data tutorial dives into additional detail regarding the other data product levels, but this tutorial will focus exclusively on flux data.

To extract the dp04 data from the HDF5 files and merge them into a single table, we'll use the stackEddy() function. We provide the function two input arguments, filepath and level. The filepath will be the file directory (dirFile) used for the data download via zipsByProduct() combined with the filestoStack00200 folder created by the function. To grab just the flux data products level = dp04:

flux <- stackEddy(filepath=paste0(dirFile,
                                  "/filesToStack00200"),
                  level="dp04")

We now have an object called flux. It's a named list containing six
tables: one table for each site's data, plus four metadata tables: variables and objDesc contain details about the data variables, issue_log contains records about changes or errors in the data, and citation contains the BibTeX citation for the downloaded data.

One of the advantages to the HDF5 files is all the metadata and data descriptions can be compiled into file as additional data tables or as attributes. The stackEddy() function stacks this contextual data in the variables and objDesc tables to help you interpret the column headers in the data table (though it's not comprehensive currently).

The terms of interest for our analysis in dp04 the three flux quantities under fluxCo2:

  • turb: Turbulent flux
  • stor: Storage
  • nsae: Net surface-atmosphere exchange

Note the variables table contains the units for each field:

flux$variables

##    category   system variable             stat           units
## 1      data  fluxCo2     nsae          timeBgn              NA
## 2      data  fluxCo2     nsae          timeEnd              NA
## 3      data  fluxCo2     nsae             flux umolCo2 m-2 s-1
## 4      data  fluxCo2     stor          timeBgn              NA
## 5      data  fluxCo2     stor          timeEnd              NA
## 6      data  fluxCo2     stor             flux umolCo2 m-2 s-1
## 7      data  fluxCo2     turb          timeBgn              NA
## 8      data  fluxCo2     turb          timeEnd              NA
## 9      data  fluxCo2     turb             flux umolCo2 m-2 s-1
## 10     data  fluxH2o     nsae          timeBgn              NA
## 11     data  fluxH2o     nsae          timeEnd              NA
## 12     data  fluxH2o     nsae             flux           W m-2
## 13     data  fluxH2o     stor          timeBgn              NA
## 14     data  fluxH2o     stor          timeEnd              NA
## 15     data  fluxH2o     stor             flux           W m-2
## 16     data  fluxH2o     turb          timeBgn              NA
## 17     data  fluxH2o     turb          timeEnd              NA
## 18     data  fluxH2o     turb             flux           W m-2
## 19     data fluxMome     turb          timeBgn              NA
## 20     data fluxMome     turb          timeEnd              NA
## 21     data fluxMome     turb         veloFric           m s-1
## 22     data fluxTemp     nsae          timeBgn              NA
## 23     data fluxTemp     nsae          timeEnd              NA
## 24     data fluxTemp     nsae             flux           W m-2
## 25     data fluxTemp     stor          timeBgn              NA
## 26     data fluxTemp     stor          timeEnd              NA
## 27     data fluxTemp     stor             flux           W m-2
## 28     data fluxTemp     turb          timeBgn              NA
## 29     data fluxTemp     turb          timeEnd              NA
## 30     data fluxTemp     turb             flux           W m-2
## 31     data     foot     stat          timeBgn              NA
## 32     data     foot     stat          timeEnd              NA
## 33     data     foot     stat      angZaxsErth             deg
## 34     data     foot     stat         distReso               m
## 35     data     foot     stat    veloYaxsHorSd           m s-1
## 36     data     foot     stat    veloZaxsHorSd           m s-1
## 37     data     foot     stat         veloFric           m s-1
## 38     data     foot     stat distZaxsMeasDisp               m
## 39     data     foot     stat      distZaxsRgh               m
## 40     data     foot     stat         distObkv               m
## 41     data     foot     stat         paraStbl               -
## 42     data     foot     stat      distZaxsAbl               m
## 43     data     foot     stat       distXaxs90               m
## 44     data     foot     stat      distXaxsMax               m
## 45     data     foot     stat       distYaxs90               m
## 46     qfqm  fluxCo2     nsae          timeBgn              NA
## 47     qfqm  fluxCo2     nsae          timeEnd              NA
## 48     qfqm  fluxCo2     nsae           qfFinl              NA
## 49     qfqm  fluxCo2     stor           qfFinl              NA
## 50     qfqm  fluxCo2     stor          timeBgn              NA
## 51     qfqm  fluxCo2     stor          timeEnd              NA
## 52     qfqm  fluxCo2     turb          timeBgn              NA
## 53     qfqm  fluxCo2     turb          timeEnd              NA
## 54     qfqm  fluxCo2     turb           qfFinl              NA
## 55     qfqm  fluxH2o     nsae          timeBgn              NA
## 56     qfqm  fluxH2o     nsae          timeEnd              NA
## 57     qfqm  fluxH2o     nsae           qfFinl              NA
## 58     qfqm  fluxH2o     stor           qfFinl              NA
## 59     qfqm  fluxH2o     stor          timeBgn              NA
## 60     qfqm  fluxH2o     stor          timeEnd              NA
## 61     qfqm  fluxH2o     turb          timeBgn              NA
## 62     qfqm  fluxH2o     turb          timeEnd              NA
## 63     qfqm  fluxH2o     turb           qfFinl              NA
## 64     qfqm fluxMome     turb          timeBgn              NA
## 65     qfqm fluxMome     turb          timeEnd              NA
## 66     qfqm fluxMome     turb           qfFinl              NA
## 67     qfqm fluxTemp     nsae          timeBgn              NA
## 68     qfqm fluxTemp     nsae          timeEnd              NA
## 69     qfqm fluxTemp     nsae           qfFinl              NA
## 70     qfqm fluxTemp     stor           qfFinl              NA
## 71     qfqm fluxTemp     stor          timeBgn              NA
## 72     qfqm fluxTemp     stor          timeEnd              NA
## 73     qfqm fluxTemp     turb          timeBgn              NA
## 74     qfqm fluxTemp     turb          timeEnd              NA
## 75     qfqm fluxTemp     turb           qfFinl              NA
## 76     qfqm     foot     turb          timeBgn              NA
## 77     qfqm     foot     turb          timeEnd              NA
## 78     qfqm     foot     turb           qfFinl              NA

3. Exploring and plotting CO2 flux data

Let's look at some data! First, we will combine the data from the two sites into a single dataframe with an additional factor variable for the site for easier data munging and to create some interesting data exploration visualizations. In this tutorial we utilize the piping operater %>% from the dplyr to allow us to quickly subset, filter, sort, and plot data using just our combined dataframe dfFlux. We start by looking at the turbulent CO2 flux timeseries at TREE with the points colored by qfFinl:

flux$STEI$Site <- "STEI"

flux$TREE$Site <- "TREE"



dfFlux <- rbind.data.frame(flux$STEI, flux$TREE)



dfFlux %>% 
    ggplot(aes(timeBgn, 
               data.fluxCo2.turb.flux, 
               color=factor(qfqm.fluxCo2.turb.qfFinl) )) +
    geom_point() + 
    scale_color_brewer(palette="Set2", 
                       name="qfFinal") +
    facet_grid(~Site) 

## Warning: Removed 1179 rows containing missing values or values outside the scale range (`geom_point()`).

When we plot the data turbulent CO2 flux data, we see a good agreement between the STEI and TREE sites. We also see some outliers that look to be due to erroneous data, especially at the STEI site. Fortunately, it looks like final quality flag (qfFinal = qfFinl) caught the largest outliers, and we can use it to remove these data from the analysis. Let's first dig into a little background information about NEON bundled EC quality flags.

Quality flags

The NEON Surface-atmosphere exchange (SAE) data products all come accompanied by a qfFinal. Information defining the quality flag (qf), quality metric (qm), and the framework to derive qfFinal are detailed in NEON EC bundled data products algorithm document and NEON Quality Flags and Quality Metrics for TIS Data Products algorithm document. Ultimately, for the dp04 flux data products (e.g. data.fluxCo2.turb.flux), qfFinal of dp01 constituent data streams derived from plausibility tests (i.e. range test, step test, spike test, and persistence test) and sensor diagnostic flags, fluxCo2 = rtioMoleDryCo2 and veloZaxsErth (dp01 data outside the scope of tutorial), are propagated and combined with theoretical eddy-covariance specific quality tests (i.e., stationarity, integral turbulence characteristics tests) to determine whether the data product is flagged as valid (qfFinal = 0) or invalid (qfFinal = 1).

Let's summarize our qfqm.fluxCo2.turb.qfFinl to see the total flagged percentage at our 2 sites.

dfFlux %>% 
  group_by(Site) %>% 
  summarise(mean(qfqm.fluxCo2.turb.qfFinl))

## # A tibble: 2 × 2
##   Site  `mean(qfqm.fluxCo2.turb.qfFinl)`
##   <chr>                            <dbl>
## 1 STEI                             0.223
## 2 TREE                             0.280

Now, lets plot the qfFinal failed percentage for all our flux data products:

dfFlux %>% 
  select(contains("qfqm")) %>% 
    pivot_longer(cols = everything(), 
                 names_to = "var") %>% 
      group_by(var) %>%  
        summarise(mean_var = mean(value) * 100) %>% 
          ggplot(aes(x = var, y = mean_var, 
                     fill = var)) + 
          geom_col() + 
          guides(x = guide_axis(angle = 90)) + 
          labs(x="Variable", 
               y="Percent qfFinal failed") + 
          scale_fill_brewer(palette="RdYlBu")

Removing data with failed qfFinal

We see that qfqm.fluxCo2.turb.qfFinl is flagged ~ 20-25% over the growing season; however, the qfqm.fluxCo2.stor.qfFinl and qfqm.fluxCo2.nsae.qfFinl are flagged quite a bit more due to the complexity of the system and propagation of issues between sub-components to the overall system (additionally, we currently employ a conservative quality control for the storage fluxes). Now that we have evaluated the qfFinal for all the fluxCo2 data streams, lets remove flagged data from our dataframe.

dfFlux %>% 
  select(contains("qfqm") & 
           contains("fluxCO2")) %>% 
  summarise(across(everything(), sum))

##   qfqm.fluxCo2.nsae.qfFinl qfqm.fluxCo2.stor.qfFinl qfqm.fluxCo2.turb.qfFinl
## 1                     9633                     7283                     4423

dfFlux %>% 
  select(contains("data") & 
           contains("fluxCO2")) %>% 
  summarise(across(everything(), 
                   function(x) 
                     {sum(is.na(x))}))

##   data.fluxCo2.nsae.flux data.fluxCo2.stor.flux data.fluxCo2.turb.flux
## 1                   3195                   2508                   1179

dfFlux$data.fluxCo2.turb.flux[(which(dfFlux$qfqm.fluxCo2.turb.qfFinal==1))] <- NA

dfFlux$data.fluxCo2.stor.flux[(which(dfFlux$qfqm.fluxCo2.stor.qfFinal==1))] <- NA

dfFlux$data.fluxCo2.nsae.flux[(which(dfFlux$qfqm.fluxCo2.nsae.qfFinal==1))] <- NA

   

dfFlux %>% 
  select(contains("qfqm") & 
           contains("fluxCO2")) %>% 
  summarise(across(everything(), sum))

##   qfqm.fluxCo2.nsae.qfFinl qfqm.fluxCo2.stor.qfFinl qfqm.fluxCo2.turb.qfFinl
## 1                     9633                     7283                     4423

dfFlux %>% 
  select(contains("data") & 
           contains("fluxCO2")) %>% 
  summarise(across(everything(), 
                   function(x) 
                     {sum(is.na(x))}))

##   data.fluxCo2.nsae.flux data.fluxCo2.stor.flux data.fluxCo2.turb.flux
## 1                   3195                   2508                   1179

We see from the summary of the fluxCo2 qfFinal and data NAs, that the number of NAs has increased as expected, we have removed all the flagged data from data.fluxCo2.turb.flux, data.fluxCo2.turb.flux, and data.fluxCo2.turb.flux. Now we have a clean data set to begin our diel flux cycle data analysis.

3. Plotting the diel cycle of CO2 fluxes

Let's look at the data in a different way, the diel cycle of carbon and water vapor are important when evaluating carbon sequestration and ecosystem growth. We can evaluate the diel cycle by binning the data by hour in the day, this is relatively straightforward using ggplot and the boxplot function (geom_boxplot()).

dfFlux$hour <- factor(lubridate::hour(dfFlux$timeBgn))



ggplot(dfFlux, aes(x = hour, 
                   y = data.fluxCo2.turb.flux, 
                   fill = Site)) +
  geom_boxplot() +
  stat_summary(fun = median, 
               geom = 'line', 
               aes(group = Site, 
                   colour = Site)) 

## Warning: Removed 1179 rows containing non-finite outside the scale range (`stat_boxplot()`).

## Warning: Removed 1179 rows containing non-finite outside the scale range (`stat_summary()`).

Nice! We get a glimpse of daily carbon dynamics, but something doesn't quite look right.

Note the timing of C uptake; the UTC time zone is clear here, where uptake occurs at times that appear to be during the night. Note that all NEON data use UTC time, aka Greenwich Mean Time. This is true across NEON's instrumented, observational, and airborne measurements. When working with NEON data, it's best to keep everything in UTC as much as possible, otherwise it's very easy to end up with data in mismatched times.

As an illustration, here we will convert to local time for clearer visualization of fluxes in their diel patterns. However, we recommend against doing this in analyses, since it is much more straightforward to keep all data in UTC.

The metadata attributes at the site level in the bundled EC HDF5 files contains fields for ZoneTime and TimeDiffUtcLt. We can use the rhdf5 package h5readAttributes() function to grab the metadata. In this analysis, both the STEI and TREE are in the same timezone. Therefore, we will only grab metadata from one file and apply to both sites.

fileMeta <- list.files(dirFile, 
                       pattern = paste0(".*", 
                                        site[1], 
                                        ".*.h5"), 
                       recursive = TRUE, 
                       full.names = TRUE)[1]



siteMeta <- h5readAttributes(fileMeta, 
                             name = site[1])



siteMeta 

## $DistZaxsCnpy
## [1] 9.5
## 
## $DistZaxsDisp
## [1] "6.2"
## 
## $DistZaxsGrndOfst
## [1] "0.758"
## 
## $DistZaxsLvlMeasTow
## [1] "0.65"  "4.79"  "8.15"  "12.30" "15.66" "22.36"
## 
## $DistZaxsTow
## [1] "21.34"
## 
## $ElevRefeTow
## [1] "475.7317"
## 
## $LatTow
## [1] 45.50895
## 
## $LonTow
## [1] -89.58636
## 
## $LvlMeasTow
## [1] 6
## 
## $`Pf$AngEnuXaxs`
## [1] "0.020097,0.011307,0.012322,0.015949,0.009383,0.007431,0.002004,0.000912,0.002459,-0.002334,0.002954,0.005751,-0.003629,-0.010646,0.001082,-0.001088,0.000915,-0.00618,-0.007461,-0.012817,-0.016043,-0.010921,-0.005637,-0.012574,-0.012343,-0.015775,-0.016883,-0.007593,0.013444,0.02007"
## 
## $`Pf$AngEnuYaxs`
## [1] "-0.024191,-0.023851,-0.017583,-0.013608,-0.006831,-0.015542,-0.002694,-0.006488,-0.007339,0.002997,0.013212,0.018439,0.017504,0.017,0.021775,0.017296,0.024619,0.028542,0.020075,0.017117,0.013442,0.018365,0.013516,0.009392,0.009889,-0.003119,-0.000946,0.009277,0.000653,-0.011161"
## 
## $`Pf$Ofst`
## [1] "0.016017,0.014884,0.000621,0.008402,0.000912,0.0087,0.007929,0.015406,0.014684,0.011638,0.022359,0.037957,0.03573,0.042482,0.041988,0.02881,0.03758,0.05214,0.034529,0.036575,0.044748,0.041439,0.033977,0.043441,0.053169,0.078641,0.07376,0.0432,0.031761,0.043055"
## 
## $TimeDiffUtcLt
## [1] -6
## 
## $TimeTube
## [1] 10.1 11.1 10.4 11.3 11.3 12.2
## 
## $TypeEco
## [1] "Aspen dominated regenerating forest"
## 
## $ZoneTime
## [1] "CST"
## 
## $ZoneUtm
## [1] "16N"

dfFlux$timeBgnLst <- dfFlux$timeBgn + 
  lubridate::hours(siteMeta$TimeDiffUtcLt)

dfFlux$hourLst <- factor(lubridate::hour(dfFlux$timeBgnLst))



ggplot(dfFlux, aes(x = hourLst, 
                   y = data.fluxCo2.turb.flux, 
                   fill = Site)) +
  geom_boxplot() +
  stat_summary(fun = median, 
               geom = 'line', 
               aes(group = Site, 
                   colour = Site)) +
  scale_y_continuous(limits = quantile(dfFlux$data.fluxCo2.turb.flux, 
                                       c(0.001, 0.999), 
                                       na.rm = TRUE))

## Warning: Removed 1213 rows containing non-finite outside the scale range (`stat_boxplot()`).

## Warning: Removed 1213 rows containing non-finite outside the scale range (`stat_summary()`).

That looks better, now we can clearly see the diel cycle of the turbulent COsub>2 flux at our STEI and TREE sites with peak carbon uptake around noon. Carbon uptake generally aligns with peak solar angle and incoming photosynthetically active radiation (PAR). The boxplot also provides us information about the variability of the fluxes throughout the day across the growing season with interquartile range (IQR) represented by the box and the whiskers indicating the threshold for outliers (black dots) as $ 1.5 x IQR$ subtracted from 1st quantile (Q1) and added to 3rd quantile (Q3):

$$

Q1 - 1.5 * IQR

$$ $$

Q3 + 1.5 * IQR

$$

In the boxplots above the outlier points were greatly reduced after the qfFinal data removal; however, some outliers remained. To focus our attention on the bulk of the data, we scaled the y-axis using scale_y_continuous() in conjunction with the quantile() function to look at 99.8% of the data excluding extreme outliers that may have remained after our filtering. We know that the turbulent flux is usually the dominant flux component to NEE, but the storage flux can be significant at forested sites. Let's have a look at the storage flux and its impact on the NSAE flux at STEI and TREE:

ggplot(dfFlux, aes(x = hourLst, 
                   y = data.fluxCo2.stor.flux, 
                   fill = Site)) +
  geom_boxplot() +
  stat_summary(fun = median, 
               geom = 'line', 
               aes(group = Site, 
                   colour = Site)) +
  scale_y_continuous(limits = quantile(dfFlux$data.fluxCo2.stor.flux, 
                                       c(0.001, 0.999), 
                                       na.rm = TRUE))

## Warning: Removed 2540 rows containing non-finite outside the scale range (`stat_boxplot()`).

## Warning: Removed 2540 rows containing non-finite outside the scale range (`stat_summary()`).

The storage flux as expected is smaller than the turbulent flux, and is dominant during the nighttime hours and evening/morning transition times when turbulence is generally weaker and the trees and understory vegetation aren't photosynthesizing. It's interesting to note the stronger signal at the TREE site. Let's look at how that impacts the overall NSAE flux:

ggplot(dfFlux, aes(x = hourLst, 
                   y = data.fluxCo2.nsae.flux, 
                   fill = Site)) +
  geom_boxplot() +
  stat_summary(fun = median, 
               geom = 'line', 
               aes(group = Site, 
                   colour = Site)) +
  scale_y_continuous(limits = quantile(dfFlux$data.fluxCo2.nsae.flux, 
                                       c(0.001, 0.999), 
                                       na.rm = TRUE))

## Warning: Removed 3225 rows containing non-finite outside the scale range (`stat_boxplot()`).

## Warning: Removed 3225 rows containing non-finite outside the scale range (`stat_summary()`).

When we look at the NSAE flux we see that the STEI site appears to sequester more carbon, which makes sense considering the STEI site is predominately a young Aspen stand for pulpwood that is growing at a fast rate. It's difficult to get a good total Carbon uptake estimate without performing some more advanced gap-filling and partitioning to better understand the ecosystem carbon dynamics. We can simply look at the relative cumulative flux contributions:

dfFlux %>% 
  select(contains("data.fluxCo2") | "Site") %>% 
      group_by(Site) %>% 
        summarise(across(everything(), mean,
                         na.rm=TRUE))

## # A tibble: 2 × 4
##   Site  data.fluxCo2.nsae.flux data.fluxCo2.stor.flux data.fluxCo2.turb.flux
##   <chr>                  <dbl>                  <dbl>                  <dbl>
## 1 STEI                   -2.44                 0.0486                  -2.47
## 2 TREE                   -1.71                -0.0242                  -1.77

The difference between the two sites is small, but on average the STEI site takes up more carbon in the turb CO2 fluxes. This obviously comes with the tradeoff that the trees are managed for cultivation in the future; whereas, TREE is currently managed as an educational resource that is likely to be preserved, which may store more carbon in the longer term.

This analysis is meant to provide a glimpse into NEON flux data products, while focusing primarily on CO2. This workflow can easily be adapted to look at Sensible and Latent heat (H2O) fluxes, or be adapted to evaluate additional NEON sites or years of data.

Get Lesson Code

flux-diel-cycle.R

Questions?

If you have questions or comments on this content, please contact us.

Contact Us
NSF NEON, Operated by Battelle

Follow Us:

Join Our Newsletter

Get updates on events, opportunities, and how NEON is being used today.

Subscribe Now

Footer

  • About Us
  • Contact Us
  • Terms & Conditions
  • Careers
  • Code of Conduct

Copyright © Battelle, 2026

The National Ecological Observatory Network is a major facility fully funded by the U.S. National Science Foundation.

Any opinions, findings and conclusions or recommendations expressed in this material do not necessarily reflect the views of the U.S. National Science Foundation.