GeoPackage vs GeoParquet vs Shapefile for Farm Data
TL;DR: Use GeoParquet as the analytical store for dense yield and sampling data, GeoPackage as the multi-layer desktop-editing container, and Shapefile only at the final export to a controller or FMIS — never as the store of record for large point datasets.
Why This Choice Matters
Picking a vector format for a farm pipeline is not a matter of taste — the wrong pick shows up as slow scans, bloated storage, corrupted attribute names, or a file that a combine’s display refuses to import. A season of 1 Hz yield logging is hundreds of thousands of points per field; kept as Shapefile, that data reads slowly, approaches the 2 GB component ceiling, and truncates any attribute name past 10 characters. Kept as GeoParquet, the same data is several times smaller and a field-clipped query reads a fraction of the file. But swing too far and store your controller export as GeoParquet, and the machine simply cannot open it. This guide resolves the three-way trade-off with a concrete matrix and a benchmark you can run on your own data.
For the full structural background on each format, this guide sits under Geospatial File Formats for Farm Data Pipelines.
The Comparison, at a Glance
| Criterion | Shapefile | GeoPackage | GeoParquet |
|---|---|---|---|
| Read speed (full layer) | Moderate | Moderate | Fast |
| Read speed (subset of columns / spatial window) | Slow (full scan) | Slow–moderate | Fast (pushdown) |
| Write speed | Fast | Moderate | Fast |
| Typical file size (dense points) | Largest | Smaller | Smallest (columnar + zstd) |
| Multi-layer in one file | No (one layer per file set) | Yes (many named layers) | No (one layer per file) |
| Cloud / predicate pushdown | No | Limited | Yes (row-group stats + bbox) |
| Random-access editing | No | Yes (SQL) | No (rewrite to change) |
| Attribute name length | 10 chars (hard cap) | Unlimited | Unlimited |
| Component / size ceiling | 2 GB per .shp/.dbf |
Practically none | Practically none |
| QGIS / desktop GIS native | Yes | Yes (first-class) | Newer support, improving |
| Equipment controller / FMIS import | Yes (de facto standard) | Rare | No |
Per-Use-Case Guidance
Field boundaries and management zones you edit by hand → GeoPackage. These are low-volume, high-value layers that agronomists open and adjust in QGIS. A single .gpkg holds boundaries, zones, and sampling points as named layers, with full-length column names and embedded CRS. Editing is transactional through SQL, and there is no sidecar to lose. Boundary cleanup that precedes this storage is covered in field boundary extraction with GeoPandas.
Dense yield-monitor points and multi-season archives → GeoParquet. This is where columnar storage pays off. Reading yield_bu_ac and geometry for one field out of a statewide archive touches a small fraction of the file thanks to column pruning and row-group skipping. Storage drops several fold versus Shapefile. This is also the safe home for data that would breach the Shapefile 2 GB ceiling.
The final prescription handed to a machine → Shapefile or ISOXML. Whatever the internal store, the export at the equipment boundary is Shapefile or ISOXML because that is what John Deere GreenStar, Trimble, Raven, and CNH controllers ingest. Rename long attribute names to short unique tokens first, and set UTF-8 encoding so operator notes survive. Validating that export against equipment expectations is the subject of shapefile validation for farm equipment.
Prerequisites
Only pyarrow differs from the parent guide’s environment:
geopandas==0.14.4
pyogrio==0.9.0
pyarrow==16.1.0
pip install geopandas==0.14.4 pyogrio==0.9.0 pyarrow==16.1.0
Step-by-Step: Benchmark the Three on Your Own Data
The comparison table is directional; the only authority is your own field-season on your own hardware. This script writes the same GeoDataFrame to all three formats, then times a realistic filtered read and records file size.
Step 1 — Load a representative layer and write all three formats
import time
import geopandas as gpd
from pathlib import Path
# A dense point layer is the fairest stress test (yield monitor logs)
src = gpd.read_file("yield_2025.gpkg", layer="points", engine="pyogrio")
assert src.crs is not None and src.crs.is_projected, "Project to UTM before benchmarking"
print(f"Rows: {len(src):,} Columns: {len(src.columns)} CRS: EPSG:{src.crs.to_epsg()}")
src.to_file("bench.shp", driver="ESRI Shapefile", engine="pyogrio", encoding="UTF-8")
src.to_file("bench.gpkg", layer="points", driver="GPKG", engine="pyogrio")
src.to_parquet("bench.parquet", compression="zstd", write_covering_bbox=True)
Step 2 — Time a column-subset read and record size
def file_size_mb(pattern: str) -> float:
return sum(p.stat().st_size for p in Path().glob(pattern)) / 1e6
def timed_read(fn) -> tuple[int, float]:
t0 = time.perf_counter()
out = fn()
return len(out), time.perf_counter() - t0
# Realistic analytical read: only geometry + one measured column
COLS = ["geometry", "yield_bu_ac"]
reads = {
"Shapefile": lambda: gpd.read_file("bench.shp", columns=COLS, engine="pyogrio"),
"GeoPackage": lambda: gpd.read_file("bench.gpkg", columns=COLS, engine="pyogrio"),
"GeoParquet": lambda: gpd.read_parquet("bench.parquet", columns=COLS),
}
sizes = {
"Shapefile": file_size_mb("bench.shp") + file_size_mb("bench.dbf")
+ file_size_mb("bench.shx") + file_size_mb("bench.prj"),
"GeoPackage": file_size_mb("bench.gpkg"),
"GeoParquet": file_size_mb("bench.parquet"),
}
print(f"{'Format':<12}{'Rows':>10}{'Read s':>10}{'Size MB':>10}")
for name, fn in reads.items():
n, secs = timed_read(fn)
print(f"{name:<12}{n:>10,}{secs:>10.3f}{sizes[name]:>10.1f}")
# Verification: every format must return the same row count and CRS
counts = {name: len(fn()) for name, fn in reads.items()}
assert len(set(counts.values())) == 1, f"Row counts diverged: {counts}"
print("All three formats agree on row count:", next(iter(counts.values())))
On a typical several-hundred-thousand-point yield layer, expect GeoParquet to be the smallest file and the fastest column-subset read, GeoPackage to sit in the middle, and the Shapefile to be the largest on disk. The exact ratios depend on how repetitive your attribute columns are — heavy repetition (season, crop, zone codes) favours GeoParquet’s dictionary encoding most.
Gotchas & Edge Cases
- Comparing full reads hides GeoParquet’s advantage. If you benchmark
read_filewithout acolumnsargument, all three walk every field and the columnar win disappears. The realistic farm workload reads a few columns or a spatial window — always benchmark that, not a naive full read. pyarrowversion drives GeoParquet features.write_covering_bboxand newer GeoParquet metadata need recentpyarrowandgeopandas. On older stacks the call silently omits the bbox column, and predicate pushdown on spatial filters quietly does nothing.- GeoPackage
columns=still scans rows. Column selection reduces what is materialised, but SQLite is row-oriented, so it does not skip row groups the way Parquet does. Do not expect GeoParquet-class scan performance from a GPKG. - Reading GeoParquet with plain pandas loses geometry and CRS. Use
geopandas.read_parquet, neverpandas.read_parquet, or the WKB column comes back as opaque bytes with no CRS attached.
Parent Guide
This guide is part of Geospatial File Formats for Farm Data Pipelines — see there for the full pipeline context, the parameter reference, and the raster (GeoTIFF/COG) side of the format decision.
Related
- Converting Shapefiles to GeoParquet with GeoPandas — the mechanics of moving a legacy Shapefile store into the GeoParquet analytical layer
- Shapefile Validation for Farm Equipment — validating the Shapefile you export at the equipment boundary
- Geospatial File Formats for Farm Data Pipelines — the parent guide with the full structural comparison and raster formats