Climate indices — catalog & behaviour (offline)¶
Inspect the bundled index catalogue and the backend's design rules
without touching the network: which indices ship, their source and
ASCII dialect, the two pure parsers, and why this backend ignores
spatial arguments and rejects aggregate=. This is the no-network
companion to the live quickstart.
import pandas as pd
from earthlens.climate_indices import (
Catalog,
ClimateIndices,
parse_climexp,
parse_psl,
)
The shipped indices¶
Catalog() loads the bundled climate_indices_data_catalog.yaml. Each
row is a frozen pydantic Index carrying its source, ASCII dialect,
file URL, units, and citation.
catalog = Catalog()
pd.DataFrame(
[
{
'id': i,
'source': catalog.get(i).source,
'dialect': catalog.get(i).dialect,
'units': catalog.get(i).units,
'long_name': catalog.get(i).long_name,
}
for i in catalog.available()
]
).set_index('id')
The two ASCII dialects¶
Each source ships a different layout. The parsers are pure text → pandas, so we can show them on tiny inline samples — no download needed.
NOAA PSL (psl): a <first_year> <last_year> header, one
year jan..dec row per year, then a lone missing-value sentinel line
(it varies per file) and a free-text footer.
psl_sample = ''' 2000 2001
2000 0.50 0.10 -0.20 0.30 0.40 -0.10 0.05 0.15 -0.25 0.35 0.45 -0.05
2001 0.20 -0.30 0.10 -99.90 -99.90 -99.90 -99.90 -99.90 -99.90 -99.90 -99.90 -99.90
-99.9
Example index — provenance footer line
'''
parse_psl(psl_sample).head(4)
The lone -99.9 line is detected as the sentinel and mapped to NaN;
the header and footer are ignored. KNMI climexp (climexp) instead
uses #-comment lines and rows of year jan..dec (a trailing
annual-mean column is dropped when present); its sentinel is -999.9.
climexp_sample = '''# title :: Example climexp index
# source :: https://climexp.knmi.nl
2000 0.11 0.22 0.33 0.44 0.55 0.66 0.77 0.88 0.99 1.00 1.11 1.22 0.69
2001 0.10 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9
'''
parse_climexp(climexp_sample).head(4)
Resolving ids — with a did-you-mean hint¶
An unknown id raises a ValueError that suggests the closest match,
rather than failing silently.
catalog.get('noo')
Global scalars: spatial arguments are ignored¶
Climate indices have no geometry, so the backend accepts a bbox for signature parity but never uses it — the spatial extent is always the whole globe. (Constructing the backend does no network I/O.)
src = ClimateIndices(
start='2000-01-01',
end='2001-12-31',
variables=['oni'],
lat_lim=[10.0, 20.0], # accepted ...
lon_lim=[30.0, 40.0], # ... but ignored
path='ci_out',
)
space = src.space
(space.south, space.north, space.west, space.east)
Nothing to grid-reduce: aggregate= is rejected¶
The values are already monthly scalars, so a non-None aggregate=
raises NotImplementedError (the check fires before any network call).
Do any rollup on the returned DataFrame instead — see the quickstart's
annual-means example.
src.download(aggregate=object())
An empty selection is an error, not a silent 'fetch everything'¶
ClimateIndices(start='2000-01-01', end='2001-12-31', variables=[])
Takeaway¶
- The catalogue maps each id → source + ASCII dialect + metadata; list
it with
Catalog().available(). - Two pure parsers (
parse_psl/parse_climexp) turn the source text into the canonicaldate / valueframe, sentinel →NaN. - The backend is tabular and global: spatial args are ignored and
aggregate=is rejected by design.