static-frame#
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: static-frame/static-frame
Docs: http://static-frame.readthedocs.io
Packages: https://pypi.org/project/static-frame
API Search: https://staticframe.dev
Installation via pip#
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]
Installation via conda#
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
Dependencies#
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
StaticFrame 5#
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 |
|
~70× |
Concatenate (axis 1) |
|
~30× |
Row-wise function application |
|
~15× |
Select columns |
|
~13× |
Set index |
|
~9× |
Ranking (with ties) |
|
~6× |
Group-by reduction |
|
~2× |
Pivot table |
|
~1.7× |
Join (unique key) |
|
~1.2× |
All examples build their data with frame_fixtures (imported as ff), and use compare() to time StaticFrame against an equivalent Pandas call:
>>> import numpy as np
>>> import pandas as pd
>>> import timeit
>>> import static_frame as sf
>>> import frame_fixtures as ff
>>> def compare(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') if min(st, pt) < 1e-3 else (1e3, 'ms')
... print(f'{label:16} StaticFrame {st*scale:7.1f} {unit} | Pandas {pt*scale:7.1f} {unit} | {pt / st:.1f}x')
No-Copy Operations on Immutable Data#
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)
rename StaticFrame 8.2 µs | Pandas 567.0 µs | 68.8x
>>> compare('set index', lambda: f1.set_index(0), lambda: df1.set_index(0, drop=False), number=2000)
set index StaticFrame 64.7 µs | Pandas 596.1 µs | 9.2x
>>> compare('select columns', lambda: f1[[10, 50, 100, 500]], lambda: df1[[10, 50, 100, 500]], number=10000)
select columns StaticFrame 5.5 µs | Pandas 69.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 (axis 1) StaticFrame 62.3 µs | Pandas 1781.9 µs | 28.6x
Faster Computation#
Version 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(lambda s: 'g' + (s % 1000).astype('U4')).assign['r'].apply(lambda s: 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-by StaticFrame 11.7 ms | Pandas 23.7 ms | 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)
reduce StaticFrame 11.6 ms | Pandas 23.8 ms | 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)
pivot StaticFrame 13.2 ms | Pandas 22.7 ms | 1.7x
>>> compare('rank', lambda: f['r'].rank_mean(), lambda: df['r'].rank(method='average'), number=20)
rank StaticFrame 18.5 ms | Pandas 108.5 ms | 5.9x
>>> compare('row-apply', lambda: f.iter_tuple(axis=1).apply(lambda t: t.x * 2 + t.y - t.z), lambda: df.apply(lambda t: t.x * 2 + t.y - t.z, axis=1), number=5)
row-apply StaticFrame 485.8 ms | Pandas 5348.0 ms | 11.0x
For 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)
join StaticFrame 35.7 ms | Pandas 42.1 ms | 1.2x
For the complete performance suite (dozens of comparisons across construction, selection, iteration, grouping, and reduction) run python -m static_frame.profile --performance "*".