Python client for the USDA-NRCS Soil Data Access (SDA) web service and other National Cooperative Soil Survey data sources.
soildb provides Python access to:
- Soil Survey Data: USDA Soil Data Access (SDA) web service for SSURGO/STATSGO
- Laboratory Data: NCSS Kellogg Soil Survey Laboratory (KSSL) characterization data
- Bulk Downloads: Complete SSURGO/STATSGO datasets from Web Soil Survey
- Multiple Backends: Query data from SDA web service, local SQLite snapshots, or GeoPackage files
Query soil survey data via web service or local database, export to pandas or polars DataFrames, and handle spatial queries.
pip install soildbFor spatial functionality:
pip install soildb[spatial]For all optional features:
pip install soildb[all]- Query SSURGO and STATSGO data from USDA Soil Data Access
- Build SQL queries with fluent
Querybuilder - Spatial queries with points, bounding boxes, and WKT polygons
- Bulk data fetching with automatic chunking and pagination
- Export to pandas, polars, and GeoPandas DataFrames
- Query NCSS Kellogg Soil Survey Laboratory (KSSL) pedon and horizon data
- Query via SDA web service or local SQLite snapshot databases
- Convert horizon data to
SoilProfileCollectionobjects
- Download complete SSURGO and STATSGO datasets as ZIP archives
- Concurrent downloads with automated file extraction
- Select by state or survey area symbol
- Query live SDA web service or offline SQLite/GeoPackage snapshots
- Unified interface across all backends
- Async and synchronous execution for all workflows
Build and execute custom SQL queries with the fluent interface:
import asyncio
from soildb import Query, SDAClient
query = (Query()
.select("mukey", "muname", "musym")
.from_("mapunit")
.inner_join("legend", "mapunit.lkey = legend.lkey")
.where("areasymbol = 'IA109'")
.limit(5))
# Inspect generated SQL
print(query.to_sql())
# Execute and get results
async def main():
async with SDAClient() as client:
result = await client.execute(query)
return result.to_pandas()
df = asyncio.run(main())
print(df.head())SELECT TOP 5 mukey, muname, musym FROM mapunit INNER JOIN legend ON mapunit.lkey = legend.lkey WHERE areasymbol = 'IA109'
mukey muname musym
0 408337 Colo silty clay loam, channeled, 0 to 2 percen... 1133
1 408339 Colo silty clay loam, 0 to 2 percent slopes 133
2 408340 Colo silty clay loam, 2 to 4 percent slopes 133B
3 408345 Clarion loam, 9 to 14 percent slopes, moderate... 138D2
4 408348 Harpster silt loam, 0 to 2 percent slopes 1595
All soildb functions have both async and synchronous versions. For most use cases, the synchronous .sync() version is simpler and easier to use.
For simple scripts and interactive use, soildb provides synchronous versions of all async functions:
from soildb import get_mapunit_by_areasymbol
# Synchronous usage - no async/await needed!
mapunits = get_mapunit_by_areasymbol.sync("IA109")
df = mapunits.to_pandas()
print(f"Found {len(df)} map units")
df.head()Found 80 map units
| mukey | musym | muname | mukind | muacres | areasymbol | areaname | |
|---|---|---|---|---|---|---|---|
| 0 | 408333 | 1032 | Spicer silty clay loam, 0 to 2 percent slopes | Consociation | 1834 | IA109 | Kossuth County, Iowa |
| 1 | 408334 | 107 | Webster clay loam, 0 to 2 percent slopes | Consociation | 46882 | IA109 | Kossuth County, Iowa |
| 2 | 408335 | 108 | Wadena loam, 0 to 2 percent slopes | Consociation | 807 | IA109 | Kossuth County, Iowa |
| 3 | 408336 | 108B | Wadena loam, 2 to 6 percent slopes | Consociation | 1103 | IA109 | Kossuth County, Iowa |
| 4 | 408337 | 1133 | Colo silty clay loam, channeled, 0 to 2 percen... | Consociation | 1403 | IA109 | Kossuth County, Iowa |
The .sync methods manage SDA client connections automatically. To execute multiple queries, reuse a client:
from soildb import SDAClient, get_mapunit_by_areasymbol
client = SDAClient()
mapunits1 = get_mapunit_by_areasymbol.sync("IA109", client=client)
mapunits2 = get_mapunit_by_areasymbol.sync("IA113", client=client)
client.close.sync()soildb provides high-level functions for common tasks:
from soildb import get_mapunit_by_point
# Point query for Ames, Iowa
response = get_mapunit_by_point.sync(-93.6319, 42.0308)
df = response.to_pandas()
print(f"Found {len(df)} records at coordinates")To request convenience functions, open an issue on GitHub.
Query soil data by location with points, bounding boxes, or polygons:
import asyncio
from soildb import SDAClient, spatial_query
async def main():
async with SDAClient() as client:
response = await spatial_query(
geometry="POINT(-93.6 42.0)",
table="mupolygon",
client=client
)
return response.to_pandas()
df = asyncio.run(main())
print(f"Point query found {len(df)} results")Point query found 1 results
| mukey | areasymbol | musym | nationalmusym | muname | mukind | |
|---|---|---|---|---|---|---|
| 0 | 411278 | IA169 | 1314 | fsz1 | Hanlon-Spillville complex, channeled, 0 to 2 p... | Complex |
Retrieve large datasets with automatic pagination and chunking:
from soildb import fetch_by_keys, get_mukey_by_areasymbol
# Get mukeys for survey areas
areas = ["IA109", "IA113", "IA117"]
all_mukeys = get_mukey_by_areasymbol.sync(areas)
print(f"Found {len(all_mukeys)} mukeys across {len(areas)} areas")
# Fetch components in chunks automatically
response = fetch_by_keys.sync(
all_mukeys,
"component",
key_column="mukey",
chunk_size=100,
columns=["mukey", "cokey", "compname", "localphase", "comppct_r"]
)
df = response.to_pandas()
print(f"Fetched {len(df)} component records")Found 410 mukeys across 3 areas
Fetching 410 keys in 5 chunks of 100
Fetched 1067 component records
| mukey | cokey | compname | localphase | comppct_r | |
|---|---|---|---|---|---|
| 0 | 408333 | 25562547 | Kingston | <NA> | 2 |
| 1 | 408333 | 25562548 | Okoboji | <NA> | 5 |
| 2 | 408333 | 25562549 | Spicer | <NA> | 90 |
| 3 | 408333 | 25562550 | Madelia | <NA> | 3 |
| 4 | 408334 | 25562837 | Okoboji | <NA> | 5 |
| 5 | 408334 | 25562838 | Glencoe | <NA> | 3 |
| 6 | 408334 | 25562839 | Canisteo | <NA> | 2 |
| 7 | 408334 | 25562840 | Webster | <NA> | 85 |
| 8 | 408334 | 25562841 | Nicollet | <NA> | 5 |
| 9 | 408335 | 25562135 | Biscay | <NA> | 1 |
In SSURGO, mukey (map unit key) is the parent and cokey (component key) is the child. Pass key_column="mukey" to fetch child components for known map units.
Download complete SSURGO and STATSGO datasets as ZIP files from the USDA Web Soil Survey portal:
from soildb import download_wss
# Download specific survey areas
paths = download_wss.sync(
areasymbols=["IA109", "IA113"],
dest_dir="./ssurgo_data",
extract=True
)
print(f"Downloaded {len(paths)} survey areas")
# Download all survey areas for a state
paths = download_wss.sync(
where_clause="areasymbol LIKE 'IA%'",
dest_dir="./iowa_ssurgo",
extract=True,
remove_zip=True
)
# Download STATSGO (general soil map) data
paths = download_wss.sync(
areasymbols=["IA"],
db="STATSGO",
dest_dir="./iowa_statsgo",
extract=True
)Extracted survey area directories contain:
tabular/: Pipe-delimited text files with soil attribute tablesspatial/: Shapefiles with map unit polygons and boundaries
Use SDA for targeted live queries. Use WSS downloads for complete offline datasets.
Use async functions directly for concurrent operations:
import asyncio
from soildb import fetch_by_keys, get_mukey_by_areasymbol
async def concurrent_example():
# Get mukeys for multiple areas concurrently
areas = ["IA109", "IA113", "IA117"]
all_mukeys = await get_mukey_by_areasymbol(areas)
# Fetch components concurrently with automatic pagination
response = await fetch_by_keys(
all_mukeys,
"component",
key_column="mukey",
chunk_size=100,
columns=["mukey", "cokey", "compname", "comppct_r"]
)
return response.to_pandas()
# Run async function
df = asyncio.run(concurrent_example())See the Async Guide for more concurrency patterns.
See docs/examples/ and Workflows for runnable code.
MIT. See LICENSE.