Skip to main content

Container library for working with tabular Arrow data

Project description

quivr

Quivr is a Python library which provides great containers for Arrow data.

Quivr's Tables are like DataFrames, but with strict schemas to enforce types and expectations. They are backed by the high-performance Arrow memory model, making them well-suited for streaming IO, RPCs, and serialization/deserialization to Parquet.

why?

Data engineering involves taking analysis code and algorithms which were prototyped, often on pandas DataFrames, and shoring them up for production use.

While DataFrames are great for ad-hoc exploration, visualization, and prototyping, they aren't as great for building sturdy applications:

  • Loose and dynamic typing makes it difficult to be sure that code is correct without lots of explicit checks of the dataframe's state.
  • Performance of Pandas operations can be unpredictable and have surprising characteristics, which makes it harder to provision resources.
  • DataFrames can use an extremely large amount of memory (typical numbers cited are between 2x and 10x the "raw" data's size), and often are forced to copy data in intermediate computations, which poses unnecessarily heavy requirements.
  • The mutability of DataFrames can make debugging difficult and lead to confusing state.

We don't want to throw everything out, here. Vectorized computations are often absolutely necessary for data work. But what if we could have those vectorized computations, but with:

  • Types enforced at runtime, with no dynamically column information.
  • Relatively uniform performance due to a no-copy orientation
  • Immutable data, allowing multiple views at very fast speed

This is what Quivr's Tables try to provide.

Installation

Check out this repo, and pip install it.

Usage

Your main entrypoint to Quivr is through defining classes which represent your tables. You write a pyarrow.Schema as the schema class attribute of your class, and Quivr will take care of the rest.

from quivr import TableBase
import pyarrow as pa


class Coordinates(TableBase):
    schema = pa.schema(
        [
            pa.field("x", pa.float64()),
            pa.field("y", pa.float64()),
            pa.field("z", pa.float64()),
            pa.field("vx", pa.float64()),
            pa.field("vy", pa.float64()),
            pa.field("vz", pa.float64()),
        ]
    )

Then, you can construct tables from data:

coords = Coordinates.from_data(
    x=np.array([ 1.00760887, -2.06203093,  1.24360546, -1.00131722]),
    y=np.array([-2.7227298 ,  0.70239707,  2.23125432,  0.37269832]),
    z=np.array([-0.27148738, -0.31768623, -0.2180482 , -0.02528401]),
    vx=np.array([ 0.00920172, -0.00570486, -0.00877929, -0.00809866]),
    vy=np.array([ 0.00297888, -0.00914301,  0.00525891, -0.01119134]),
    vz=np.array([-0.00160217,  0.00677584,  0.00091095, -0.00140548])
)

# Sort the table by the z column. This returns a copy.
coords_z_sorted = coords.sort_by("z")

print(len(coords))
# prints 4

# Access any of the columns as a numpy array with zero copy:
xs = coords.x.to_numpy()

# Present the table as a pandas DataFrame, with zero copy if possible:
df = coords.to_dataframe()

Embedded definitions and nullable fields

You can embed one table's definition within another, and you can make fields nullable:

class AsteroidOrbit(TableBase):
    schema = pa.schema(
        [
            pa.field("designation", pa.string()),
            pa.field("mass", pa.float64(), nullable=True),
            pa.field("radius", pa.float64(), nullable=True),
            Coordinates.as_field("coords"),
        ]
    )

# You can construct embedded fields from Arrow StructArrays, which you can get from
# other Quivr tables using the to_structarray() method with zero copy.
orbits = AsteroidOrbit.from_data(
    designation=np.array(["Ceres", "Pallas", "Vesta", "2023 DW"]),
    mass=np.array([9.393e20, 2.06e21, 2.59e20, None]),
    radius=np.array([4.6e6, 2.7e6, 2.6e6, None]),
    coords=coords.to_structarray(),
)

Computing

You can use the columns of the data to do computations:

import pyarrow.compute as pc

median_mass = pc.quantile(orbits.mass, q=0.5)
# median_mass is a pyarrow.Scalar, which you can get the value of with .as_py()
print(median_mass.as_py())

There is a very extensive set of functions available in the pyarrow.compute package, which you can see here. These computations will, in general, use all cores available and do vectorized computations which are very fast.

Customizing behavior with methods

Because Quivr tables are just Python classes, you can customize the behavior of your tables by adding or overriding methods. For example, if you want to add a method to compute the total mass of the asteroids in the table, you can do so like this:

class AsteroidOrbit(TableBase):
    schema = pa.schema(
        [
            pa.field("designation", pa.string()),
            pa.field("mass", pa.float64(), nullable=True),
            pa.field("radius", pa.float64(), nullable=True),
            Coordinates.as_field("coords"),
        ]
    )

    def total_mass(self):
        return pc.sum(self.mass)

You can also use this to add "meta-fields" which are combinations of other fields. For example:

class CoordinateCovariance(TableBase):
    schema = pa.schema(
        [
            # The covariance matrix of the coordinates as a 6x6 matrix (3 positions, 3 velocities)
            pa.field("matrix_values", pa.list_(pa.float64(), 36)),
        ]
    )

    @property
    def matrix(self):
        # This is a numpy array of shape (n, 6, 6)
        return self.matrix_values.to_numpy().reshape(-1, 6, 6)


class AsteroidOrbit(TableBase):
    schema = pa.schema(
        [
            pa.field("designation", pa.string()),
            pa.field("mass", pa.float64(), nullable=True),
            pa.field("radius", pa.float64(), nullable=True),
            Coordinates.as_field("coords"),
            CoordinateCovariance.as_field("covariance"),
        ]
    )



orbits = load_orbits() # Analogous to the example above

# Compute the determinant of the covariance matrix for each asteroid
determinants = np.linalg.det(orbits.covariance.matrix)

Filtering

You can also filter by expressions on the data. See Arrow documentation for more details. You can use this to construct a quivr Table using an appropriately-schemaed Arrow Table:

big_orbits = AsteroidOrbit(orbits.table.filter(orbits.table["mass"] > 1e21))

If you're plucking out rows that match a single value, you can use the "select" method on the Table:

# Get the orbit of Ceres
ceres_orbit = orbits.select("designation", "Ceres")

Indexes for Fast Lookups

If you're going to be doing a lot of lookups on a particular column, it can be useful to create an index for that column. You can do using the quivr.StringIndex class to build an index for string values:

# Build an index for the designation column
designation_index = quivr.StringIndex(orbits, "designation")

# Get the orbit of Ceres
ceres_orbit = designation_index.lookup("Ceres")

The lookup method on the StringIndex returns Quivr Tables, or None if there is no match. Keep in mind that the returned tables might have multiple rows if there are multiple matches.

TODO: Add numeric and time-based indexes.

Serialization

Feather

Feather is a fast, zero-copy serialization format for Arrow tables. It can be used for interprocess communication, or for working with data on disk via memory mapping.

orbits.to_feather("orbits.feather")

orbits_roundtripped = AsteroidOrbit.from_feather("orbits.feather")

# use memory mapping to work with a large file without copying it into memory
orbits_mmap = AsteroidOrbit.from_feather("orbits.feather", memory_map=True)

Parquet

You can serialize your tables to Parquet files, and read them back:

orbits.to_parquet("orbits.parquet")

orbits_roundtripped = AsteroidOrbit.from_parquet("orbits.parquet")

See the Arrow documentation for more details on the Parquet format used.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

quivr-0.1.1.tar.gz (15.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

quivr-0.1.1-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

File details

Details for the file quivr-0.1.1.tar.gz.

File metadata

  • Download URL: quivr-0.1.1.tar.gz
  • Upload date:
  • Size: 15.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-httpx/0.24.0

File hashes

Hashes for quivr-0.1.1.tar.gz
Algorithm Hash digest
SHA256 183d4b609527d6addd49017a671dda45cd0c4e052751addaaa83a683ad2e9ff6
MD5 7532f03db3f2616076f2e3e49dfe930a
BLAKE2b-256 d4a87159e16b5337bbf3009b25eb79bc98500e0ad47006bbf41a82d9b3212c77

See more details on using hashes here.

File details

Details for the file quivr-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: quivr-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 28.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-httpx/0.24.0

File hashes

Hashes for quivr-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 36496db56d552d85e68fae488c469dea7709be57974dba4b10ad49c568b191a0
MD5 5e78c507510bfae64163ee6448a0ff9e
BLAKE2b-256 356caa515b4bb75bcfd8174573c1b9bcf07e020369e73647803cf834bf376b39

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page