Loading data
Load a dataset's actual rows into pandas, polars, or DuckDB, run SQL directly against it, or export to CSV — plus how the API key and row limits work.
Loading a dataset's actual rows (.to_pandas(), .to_polars(), .to_duckdb(),
.sql(), .to_csv()) requires a read-scoped API key — unless the
dataset is currently keyless (ds.keyless), in which case no key is
needed. Create a key from your Aesops account's API keys settings (see
also the API's Authentication page).
client = Client(api_key="Aes_...")
ds = client.load_dataset("kenya-housing-prices")Your own unpublished datasets
load_dataset() also works for a dataset you've uploaded but not published, as
long as the client has an API key from the account that owns it. Anyone else
gets a not-found error.
pandas
df = ds.to_pandas()Requires pandas (pip install 'aesops[pandas]').
polars
lf = ds.to_polars()Requires polars (pip install 'aesops[polars]').
DuckDB
to_duckdb() returns an in-memory DuckDB connection with a data view bound
to the dataset. DuckDB's httpfs extension does predicate + projection
pushdown, so only the row-groups your query actually touches are fetched:
con = ds.to_duckdb()
con.sql("SELECT county, avg(price) FROM data GROUP BY county").df()Several datasets in one connection
To query datasets together (joins, etc.), create one connection and pass it
in with a view_name. The view is created directly in your connection and is
still read lazily from Parquet:
import duckdb
con = duckdb.connect()
housing = client.load_dataset("kenya-housing-prices")
census = client.load_dataset("knbs-census-2019")
housing.to_duckdb(con=con, view_name="housing")
census.to_duckdb(con=con, view_name="census")
con.sql("SELECT * FROM housing JOIN census USING (county)").df()view_name defaults to the dataset's slug (quote it in SQL if it contains
hyphens, e.g. "kenya-housing-prices"). Calling it again for the same name
refreshes the view. ds.sql(query, con=con, view_name=...) works the same
way. Keep the Client open for as long as you use the connection — see
Context manager.
Long sessions
Each view reads from a signed URL that expires after about an hour. ds.sql()
and to_pandas() refresh it automatically, but queries you run yourself on a
connection (con.sql(...)) can't. If one starts failing with an HTTP 403
error, call ds.to_duckdb(con=con, view_name=...) again — it replaces the
view with a fresh URL.
New to DuckDB or DuckDBPyRelation? See the DuckDB primer.
Running SQL directly
ds.sql() skips the intermediate con — same pushdown behavior, reference
the dataset as data. Returns a DuckDBPyRelation; call .df() for pandas
or .pl() for polars:
ds.sql("SELECT county, avg(price) FROM data GROUP BY 1").df()
# anything more specific than a simple limit — offset, filters, ordering —
# also goes through ds.sql() directly
ds.sql("SELECT * FROM data ORDER BY price DESC LIMIT 20").df()CSV export
Client-side conversion — no extra server compute, DuckDB reads the Parquet directly and writes the CSV locally:
ds.to_csv("/tmp/housing.csv")Limiting rows
to_pandas(), to_polars(), and to_csv() all take a limit kwarg. It's
pushed down through read_parquet, so it also cuts what crosses the network
— not just what lands in the DataFrame or file:
sample = ds.to_pandas(limit=100)
ds.to_polars(limit=100)
ds.to_csv("/tmp/sample.csv", limit=100)