Immutable and statically-typeable DataFrames with runtime type and data validation.
Among the many Python DataFrame libraries, StaticFrame is an alternative that prioritizes correctness, maintainability, and reducing opportunities for error. Key features include:
- 🛡️ Immutable Data: Provides memory efficiency, excellent performance, and prohibits side effects.
- 🗜️ Static Typing: Use Python type-hints to statically type index, columns, and columnar types.
- 🚦 Runtime Validation: Use type hints and specialized validators for runtime type and data checks.
- 🧭 Consistent Interface: An easy-to-learn, hierarchical, and intuitive API that avoids the many inconsistencies of Pandas.
- 🧬 Comprehensive
dtypeSupport: Full compatibility with all NumPy dtypes and datetime64 units. - 🔗 Broad Interoperability: Translate between Pandas, Arrow, Parquet, CSV, TSV, JSON, Excel XLSX, SQLite, and NumPy; output to xarray, VisiData, HTML, RST, Markdown, LaTeX, and Jupyter notebooks.
- 🚀 Optimized Serialization & Memory Mapping: Fast disk I/O with custom NPZ and NPY encodings.
- 💼 Multi-Table Containers: The
BusandYarnprovide interfaces to collections of tables with lazy data loading, well-suited for large datasets. - ⏳ Deferred Processing: The
Batchprovides a common interface for deferred processing of groups, windows, or any iterator. - 📚 Comprehensive Documentation: All API endpoints documented with thousands of easily runnable examples.
Code: https://github.com/static-frame/static-frame
Docs: http://static-frame.readthedocs.io
Packages: https://pypi.org/project/static-frame
API Search: https://staticframe.dev
Install StaticFrame with pip. Note that pre-built wheels are published for all supported Python versions and platforms (including Apple Silicon platforms):
pip install static-frame
To install optional dependencies for full support of input and output formats (such as XLSX and Parquet) via pip:
pip install static-frame [extras]
StaticFrame can be installed via conda with the conda-forge channel. Note that pre-built wheels of StaticFrame and all compiled dependencies are available through pip and may offer more compatibility than a conda-based installation
conda install -c conda-forge static-frame
Core StaticFrame requires the following:
- Python>=3.10
- numpy>=1.24.3 (numpy>=2 is supported)
- arraykit==1.12.0
- typing-extensions>=4.12.0
For extended input and output, the following packages are required:
- pandas>=1.1.5
- xlsxwriter>=1.1.2
- openpyxl>=3.0.9
- xarray>=0.13.0
- pyarrow>=3.0.0
- visidata>=2.4
Make it work, make it right, make it fast: after years of making it right, StaticFrame 5 makes it fast.
With further integration of performance-critical routines in C (provided by ArrayKit ), many more operations in StaticFrame now outperform Pandas, all while preserving StaticFrame's immutable data model and its consistent, explicit interfaces.
The table below shows representative speed-ups measured on Python 3.14, NumPy 2.4, and Pandas 3.0.5. Examples are reproducible with the self-contained benchmarks that follow. Note that performance results can be highly variable based on specific data shapes and types, and any claim of "always faster" is dubious.
| Operation | StaticFrame interface | Speed-up |
|---|---|---|
| Rename axis | Frame.rename(...) | ~70× |
| Concatenate (axis 1) | Frame.from_concat(...) | ~30× |
| Row-wise function application | Frame.iter_tuple(axis=1).apply(...) | ~15× |
| Select columns | Frame[[...]] | ~13× |
| Set index | Frame.set_index(...) | ~9× |
| Ranking (with ties) | Series.rank_mean(...) | ~6× |
| Group-by reduction | Frame.iter_group(...).reduce.from_label_map(...) | ~2× |
| Pivot table | Frame.pivot(...) | ~1.7× |
| Join (unique key) | Frame.join_left(...) | ~1.2× |
All examples build their data with frame_fixtures (imported as ff), and use compare() to time StaticFrame against an equivalent Pandas call:
>>>importnumpyasnp>>>importpandasaspd>>>importtimeit>>>importstatic_frameassf>>>importframe_fixturesasff>>>defcompare(label, sf_call, pd_call, *, number):
... sf_call(); pd_call() # warm-up
... st=timeit.timeit(sf_call, number=number) /number
... pt=timeit.timeit(pd_call, number=number) /number
... scale, unit= (1e6, 'µs') ifmin(st, pt) <1e-3else (1e3, 'ms')
... print(f'{label:16} StaticFrame {st*scale:7.1f}{unit} | Pandas {pt*scale:7.1f}{unit} | {pt/st:.1f}x')Because all StaticFrame data is immutable, arrays can be safely shared between containers without defensive copies or complicated copy-on-write (CoW) management. Structural operations (relabeling, selecting columns, setting an index, concatenating) reuse the same underlying NumPy arrays and are often an order of magnitude (or more) faster than Pandas.
>>>f1=ff.parse('s(10_000,1000)|v(int,int,str,float)')
>>>f2=ff.parse('s(10_000,1000)|v(int,bool,bool,float)')
>>>df1, df2=f1.to_pandas(), f2.to_pandas()
>>>compare('rename', lambda: f1.rename(index='foo'), lambda: df1.rename_axis('foo'), number=10000)
renameStaticFrame8.2µs|Pandas567.0µs|68.8x>>>compare('set index', lambda: f1.set_index(0), lambda: df1.set_index(0, drop=False), number=2000)
setindexStaticFrame64.7µs|Pandas596.1µs|9.2x>>>compare('select columns', lambda: f1[[10, 50, 100, 500]], lambda: df1[[10, 50, 100, 500]], number=10000)
selectcolumnsStaticFrame5.5µs|Pandas69.0µs|12.6x>>>compare('concat (axis 1)', lambda: sf.Frame.from_concat((f1, f2), axis=1, columns=sf.IndexAutoFactory), lambda: pd.concat((df1, df2), axis=1), number=2000)
concat (axis1) StaticFrame62.3µs|Pandas1781.9µs|28.6xVersion 5 extends this performance to commonly used group-by, reduce, pivot, join and related operations. A single one-million-row fixture serves these examples:
>>>f= (ff.parse('s(1_000_000,5)|v(int,int,float,float,float)').relabel(columns=('key', 'r', 'x', 'y', 'z')).assign['key'].apply(lambdas: 'g'+ (s%1000).astype('U4')).assign['r'].apply(lambdas: s%100_000))
>>>df=f.to_pandas()
>>>compare('group-by', lambda: f.iter_group('key').reduce.from_label_map({'x': np.sum, 'y': np.sum}).to_frame(), lambda: df.groupby('key')[['x', 'y']].sum(), number=10)
group-byStaticFrame11.7ms|Pandas23.7ms|2.0x>>>compare('reduce', lambda: f.iter_group('key').reduce.from_label_map({'x': np.sum, 'y': np.max}).to_frame(), lambda: df.groupby('key').agg({'x': 'sum', 'y': 'max'}), number=10)
reduceStaticFrame11.6ms|Pandas23.8ms|2.0x>>>compare('pivot', lambda: f.pivot('key', data_fields='x', func=np.sum), lambda: df.pivot_table(index='key', values='x', aggfunc='sum'), number=10)
pivotStaticFrame13.2ms|Pandas22.7ms|1.7x>>>compare('rank', lambda: f['r'].rank_mean(), lambda: df['r'].rank(method='average'), number=20)
rankStaticFrame18.5ms|Pandas108.5ms|5.9x>>>compare('row-apply', lambda: f.iter_tuple(axis=1).apply(lambdat: t.x*2+t.y-t.z), lambda: df.apply(lambdat: t.x*2+t.y-t.z, axis=1), number=5)
row-applyStaticFrame485.8ms|Pandas5348.0ms|11.0xFor join, two frames are built sharing the same 500,000 integer keys, relabeled as strings:
>>>left=ff.parse('s(500_000,1)|v(float)|i(I,int)').relabel(columns=('lv',))
>>>right=ff.parse('s(500_000,1)|v(float)|i(I,int)').relabel(columns=('rv',))
>>>keys='k'+left.index.values.astype('U12')
>>>sf_left=left.relabel(index=keys).sort_index()
>>>sf_right=right.relabel(index=keys)
>>>df_left, df_right=sf_left.to_pandas(), sf_right.to_pandas()
>>>compare('join', lambda: sf_left.join_left(sf_right, left_depth_level=0, right_depth_level=0), lambda: df_left.join(df_right, how='left'), number=10)
joinStaticFrame35.7ms|Pandas42.1ms|1.2xFor the complete performance suite (dozens of comparisons across construction, selection, iteration, grouping, and reduction) run python -m static_frame.profile --performance "*".