Read data#
Note
The output under each example on this page is from runs on CHPC on 26 October 2026, except under “One pollutant from several sites” (the HDP example, 14 October 2026). See why the output is recorded.
Your first data reads one site. This page shows the other reads: several pollutants and several sites in one table, everything an instrument logged at any level, and reads of a year or more. Three functions do the reading:
Function |
What it gives |
|---|---|
One site’s concentrations at the |
|
One pollutant from several sites, in one table, with the site, instrument and position of each row. |
|
Everything one or more instruments logged, at any level, as one table per instrument. |
The arguments they share (site, pollutants, time range and the rest) are described once, on Input parameters. Every time is UTC (see Times are UTC).
Several pollutants from one site#
Name the pollutants in a list. WBB measures ozone with another instrument than CO2 and CH4:
>>> import pandas as pd
>>> import uataq
>>> obs = uataq.get_obs("WBB", ["CO2", "CH4", "O3"], time_range="2025-06-02")
>>> obs.head(3)
CO2d_ppm_cal CO2d_ppm_raw CH4d_ppm_cal CH4d_ppm_raw O3_ppb instrument
Time_UTC
2025-06-02 00:00:01.000 431.852558 434.8026 1.981142 1.979105 NaN lgr_ugga
2025-06-02 00:00:01.740 NaN NaN NaN NaN 58.7 teledyne_t400
2025-06-02 00:00:03.780 NaN NaN NaN NaN 58.6 teledyne_t400
>>> obs.groupby("instrument").count()
CO2d_ppm_cal CO2d_ppm_raw CH4d_ppm_cal CH4d_ppm_raw O3_ppb
instrument
lgr_ugga 8390 8390 8390 8390 0
teledyne_t400 0 0 0 0 41937
Each instrument logs on its own times, so a row has values from one
instrument, named in the instrument column, and is empty in the other
columns. The CO2 and CH4 analyzer logs about every 10 seconds, the ozone monitor
about every 2. To put them on common times, average them over a period, as under
Your first data (“Hourly means”):
obs.resample("h").mean(numeric_only=True).
With format="long", the table has one row per measurement instead, with the
column name in pollutant, the measurement in value and the instrument in
instrument:
>>> long = uataq.get_obs("WBB", ["CO2", "CH4", "O3"], time_range="2025-06-02", format="long")
>>> long.head(5)
pollutant value instrument
Time_UTC
2025-06-02 00:00:01.000 CO2d_ppm_cal 431.852558 lgr_ugga
2025-06-02 00:00:01.000 CO2d_ppm_raw 434.802600 lgr_ugga
2025-06-02 00:00:01.000 CH4d_ppm_cal 1.981142 lgr_ugga
2025-06-02 00:00:01.000 CH4d_ppm_raw 1.979105 lgr_ugga
2025-06-02 00:00:01.740 O3_ppb 58.700000 teledyne_t400
Several sites and pollutants#
For several sites, read each one and stack them. This reads June 2025 at three
sites in Mountain time (as under A month in local time), averages each
to hourly means (numeric_only=True leaves out the text instrument
column), and stacks them with the site ID as the first level of the
index:
>>> frames = {}
>>> for sid in ["WBB", "CSP", "HPL"]:
... obs = uataq.get_obs(sid, ["CO2", "CH4"], time_range=["2025-06-01", "2025-07-01"])
... obs = obs.tz_localize("UTC").tz_convert("America/Denver").loc["2025-06"]
... frames[sid] = obs.resample("h").mean(numeric_only=True)
...
>>> hourly = pd.concat(frames, names=["SID"])
>>> hourly.head(3)
CO2d_ppm_cal CO2d_ppm_raw CH4d_ppm_cal CH4d_ppm_raw
SID Time_UTC
WBB 2025-06-01 00:00:00-06:00 441.390343 444.442363 2.003455 2.003101
2025-06-01 01:00:00-06:00 440.615488 443.567585 1.996306 1.995796
2025-06-01 02:00:00-06:00 435.172711 437.986351 1.995488 1.994749
>>> hourly.groupby(level="SID").size()
SID
CSP 720
HPL 720
WBB 720
dtype: int64
It took 2 seconds. Every site’s hourly means are on the same hours, so one pollutant can be laid out with a column per site:
>>> hourly["CH4d_ppm_cal"].unstack("SID").head(3)
SID WBB CSP HPL
Time_UTC
2025-06-01 00:00:00-06:00 2.003455 2.293712 2.035616
2025-06-01 01:00:00-06:00 1.996306 2.433641 2.013554
2025-06-01 02:00:00-06:00 1.995488 2.233682 2.049384
This loop is the way to read a few pollutants from a few sites. Use
get_network_obs(), below, for one pollutant when you need the position,
height or instrument of each row.
A site with no data in the range raises an error (see When there is nothing to read, which shows a loop that skips such a site). Find sites and data says which sites measure a pollutant.
One pollutant from several sites#
get_network_obs() reads one pollutant from several sites and stacks the
rows in time, with the site and instrument of each row and where it was
measured:
>>> net = uataq.get_network_obs(["WBB", "CSP", "HPL"], "CH4", time_range="2025-06-02")
>>> net[["SID", "instrument", "CH4d_ppm_cal", "zagl"]].head(3)
SID instrument CH4d_ppm_cal zagl
Time_UTC
2025-06-02 00:00:01 WBB lgr_ugga 1.981142 35
2025-06-02 00:00:07 HPL lgr_ugga 1.967279 4
2025-06-02 00:00:11 WBB lgr_ugga 1.980728 35
>>> net.groupby("SID").size()
SID
CSP 6226
HPL 8427
WBB 8390
dtype: int64
The table is a geopandas.GeoDataFrame. Besides the concentration columns
(Column names), each row has its SID, its instrument, its
position (Latitude_deg, Longitude_deg and geometry) and zagl,
the height of the measurements above ground level. It takes one pollutant per call:
several pollutants at sites with different instruments have no common shape.
Network.get_obs() does the same from a Network you already made.
A site with nothing in the range is left out without an error. HDP stopped in February 2025, so asking for June 2025 returns the other two sites:
>>> net = uataq.get_network_obs(["WBB", "HDP", "CSP"], "CO2", time_range="2025-06")
>>> sorted(net["SID"].unique())
['CSP', 'WBB']
Check the sites you got back.
Which instrument a row came from#
get_obs() reads every instrument at the site that measures the pollutant,
and its instrument column says which one logged each row (the same name
get_network_obs() gives). Most sites have one instrument for each pollutant
at a time, but some have had two, one after another or at once (WBB measured NO2
with two in 2022):
>>> no2 = uataq.get_obs("WBB", "NO2", time_range="2022-03-01")
>>> no2["instrument"].value_counts()
instrument
teledyne_t200 40282
teledyne_t500u 40282
Name: count, dtype: int64
To read one instrument alone, use read_data() with its name.
Sites and instruments lists the instruments of every site,
with their dates. A mobile site has the column too; at TRX01 it
is how you tell lgr_ugga from lgr_ugga_manual_cal (see
Mobile sites).
Everything an instrument logged#
read_data() reads instruments by name and gives a dictionary with one
table for each, at the level you ask for (see
Data levels and flags):
>>> data = uataq.read_data("WBB", "lgr_ugga", lvl="qaqc", time_range="2025-06-02")
>>> list(data)
['lgr_ugga']
>>> data["lgr_ugga"].shape
(8845, 25)
The qaqc table has every row the analyzer logged and all 25 of its columns
(Columns says what they are). With no lvl, each instrument
is read at the highest level it has. With no instrument named, it reads every
instrument at the site; the ones with nothing in the range are left out, and
the log says why (see When there is nothing to read):
>>> data = uataq.read_data("WBB", time_range="2025-06-02")
>>> {name: table.shape for name, table in data.items()}
{'lgr_ugga': (8390, 5), 'teledyne_t200': (41937, 3), 'teledyne_t300': (41937, 1), 'teledyne_t400': (41937, 1)}
Sites as objects#
get_obs, read_data and get_recent_obs take the site ID first and pass
the call on to a site object, which you can also get yourself: uataq.get_obs("WBB", ...) is
uataq.get_site("WBB").get_obs(...), and the same for the other two. A site object also says what the site measures and which
instruments it has (see Which sites measure what).
A year or more#
A year of CO2 and CH4 at WBB at the final level is 2.8 million rows. It took
4 seconds to read and about 0.6 GB of memory. For a year or more of several
sites, run your script in a Slurm job (see Use uataq in a Slurm job), not on a login
node, and average to hourly means as you read each site, as above, so the
table you keep stays small.
num_processes reads an instrument’s files in several processes at once (see
Number of processes). It helps most when there are many files, as at
the raw level and in Network.get_availability() (see
Find sites and data), and it also speeds up the final level.
On a compute node on 29 October 2026, WBB’s CO2 and CH4 for 2024 (2.8 million
rows) took 3.4 seconds with one process, 2.3 with two, 1.3 with four and 1.0 with
eight; six years of CO2 (16 million rows) took 21, 7.0 and 4.7 seconds with one,
four and eight. Ask for no more processes than cores you were given. A script that uses more than one process needs the
__main__ guard, or Python 3.14 stops with a RuntimeError about
“bootstrapping” (Python 3.14 starts the workers with forkserver, and a script read
from stdin cannot spawn them, so run a script file):
import uataq
def main():
obs = uataq.get_obs("WBB", "CO2", time_range="2024", num_processes=4)
...
if __name__ == "__main__":
main()
Other data#
uataq.sodar reads the wind profiles of the horel group’s SODAR stations.
They are not UATAQ sites, so they are read with Sodar
rather than the functions on this page; its page has an example.
Next#
Data levels and flags: the levels, the flags, and what
finalkeeps.Mobile sites: the TRAX trains and the buses, and where each measurement was.
When there is nothing to read: what a read that finds nothing tells you.