Section 3: Queries and Expressions
Welcome to Section 3 of the Pixeltable Fundamentals tutorial, Queries and Expressions. In the previous section of this tutorial, Computed Columns, we saw how to issue queries over Pixeltable tables, such as:pop_t.pop_2023 - pop_t.pop_2022. We’ve seen a number of other
expressions as well, such as the chain of image operations
select() statement, which will cause
it to be evaluated dynamically, or in an add_column() statement, which
will add it to the table schema as a computed column. In this section,
we’ll dive deeper into the different kinds of Pixeltable expressions and
their uses. We’ll:
- Understand the relationship between Pixeltable expressions and query execution
- Survey the different types of expressions
- Learn more about the Pixeltable type system
create_table() with the source parameter to load it into a
Pixeltable table.
| image | label |
|---|---|
| 5 | |
| 0 | |
| 4 | |
| 1 | |
| 9 |
Column References
The most basic type of expression is a column reference: that’s what you get when you type, say,t.image. An expression such as t.image
by itself is just a Python object; it doesn’t contain any actual data,
and no data will be loaded until you use the expression in a select()
query or add_column() statement. Here’s what we get if we type
t.image by itself:
| Column 'image' (of table 'demo.mnist') |
| Column Name | Type | Computed With |
|---|---|---|
| image | Image |
JSON Collections (Dicts and Lists)
Data is commonly presented in JSON format: for example, API responses and model output often take the shape of JSON dictionaries or lists of dictionaries. Pixeltable has native support for JSON accessors. To demonstrate this, let’s add a computed column that runs an image classification model against the images in our dataset.| image | classification |
|---|---|
| {"labels": [5, 3, 2, 8, 7], "scores": [0.981, 0.013, 0.002, 0.001, 0.001], "label_text": ["5", "3", "2", "8", "7"]} | |
| {"labels": [0, 6, 9, 8, 1], "scores": [0.997, 0., 0., 0., 0.], "label_text": ["0", "6", "9", "8", "1"]} | |
| {"labels": [4, 1, 9, 7, 0], "scores": [0.997, 0.001, 0., 0., 0.], "label_text": ["4", "1", "9", "7", "0"]} |
classification column is pxt.Json:
| table 'demo.mnist' |
| Column Name | Type | Computed With |
|---|---|---|
| image | Image | |
| label | String | |
| classification | Json | vit_for_image_classification(image, model_id='farleyknight-org-username/vit-base-mnist') |
Json-typed output that
behave just as you’d expect. To look up a key in a dictionary, use the
syntax t.classification['labels']:
| classification_labels |
|---|
| [5, 3, 2, 8, 7] |
| [0, 6, 9, 8, 1] |
| [4, 1, 9, 7, 0] |
| classification_labels |
|---|
| [5, 3, 2, 8, 7] |
| [0, 6, 9, 8, 1] |
| [4, 1, 9, 7, 0] |
t.classification.labels is another Pixeltable expression; you can
think of it as saying, “do the 'labels' lookup from every dictionary
in the column t.classification, and return the result as a new
column.” As before, the expression by itself contains no data; it’s the
query that does the actual work of retrieving data. Here’s what we see
if we just give the expression by itself, without a query:
| classification_labels0 |
|---|
| 5 |
| 0 |
| 4 |
| classification_labels2 |
|---|
| [5, 3] |
| [0, 6] |
| [4, 1] |
None output for that row.
| classification_notakey |
|---|
| None |
| None |
| None |
| table 'demo.mnist' |
| Column Name | Type | Computed With |
|---|---|---|
| image | Image | |
| label | String | |
| classification | Json | vit_for_image_classification(image, model_id='farleyknight-org-username/vit-base-mnist') |
| pred_label | Json | classification.label_text[0] |
| image | col_1 |
|---|---|
| {"ground_truth": "5", "prediction": "5", "is_correct": true, "engine": "pixeltable"} | |
| {"ground_truth": "0", "prediction": "0", "is_correct": true, "engine": "pixeltable"} | |
| {"ground_truth": "4", "prediction": "4", "is_correct": true, "engine": "pixeltable"} | |
| {"ground_truth": "1", "prediction": "1", "is_correct": true, "engine": "pixeltable"} | |
| {"ground_truth": "9", "prediction": "9", "is_correct": true, "engine": "pixeltable"} |
UDF Calls
UDF calls are another common type of expression. We’ve seen them throughout the tutorial; in fact, we used one a moment ago when we added a model invocation to our workload:vit_for_image_classification UDF in the
pxt.functions.huggingface module. Note that
vit_for_image_classification is a Pixeltable UDF, not an ordinary
Python function. (UDFs were first discussed in the Tables and Data
Operations
section of this tutorial.) You can think of a Pixeltable UDF as a
function that operates on columns of data, iteratively applying an
underlying operation to each row in the column (or columns). In this
case, vit_for_image_classification operates on t.image, running the
model against every image in the column.
Notice that in addition to the column t.image, this call to
vit_for_image_classification also takes a constant argument specifying
the model_id. Any UDF call argument may be a constant, and the
constant value simply means “use this value for every row being
evaluated”.
You can always compose Pixeltable expressions to form more complicated
ones; here’s an example that runs the model against a 90-degree rotation
of every image in the sample and extracts the label. Not surprisingly,
the model doesn’t perform as well on the rotated images.
| image | rot_label |
|---|---|
| 4 | |
| 0 | |
| 5 | |
| 1 | |
| 8 |
rot_model_result for later reuse. Every
Pixeltable expression is a Python object, so you can freely assign them
to variables, reuse them, compose them, and so on. Remember that nothing
actually happens until the expression is used in a query - so in this
example, setting the variable rot_model_result doesn’t
itself result in any data being retrieved; that only happens later, when
we actually use it in the select() query.
There are a large number of built-in UDFs that ship with Pixeltable; you
can always refer back to the API
Documentation for details.
Method Calls
Many built-in UDFs allow a convenient alternate syntax. The following two expressions are exactly equivalent:a and b can always be used interchangeably in queries, with
identical results. Just like in standard Python classes, whenever
Pixeltable sees the method call t.image.rotate(90), it interprets
it as a function call pxt.functions.image.rotate(self, 90), with
(in this case) self equal to t.image.
Any method call can also be written as a function call, but (just like
in standard Python) not every function call can be written as a method
call. For example, the following won’t work:
vit_for_image_classification is part of the
pxt.functions.huggingface module, not the core module
pxt.functions.image. Most Pixeltable types have a corresponding core
module of UDFs that can be used as method calls (pxt.functions.image
for Image; pxt.functions.string for String; and so on), described
fully in the API
Documentation.
Arithmetic and Boolean Operations
Expressions can also be combined using standard arithmetic and boolean operators. As with everything else, arithmetic and boolean expressions are operations on columns that (when used in a query) are applied to every row.| image | label | col_2 | col_3 |
|---|---|---|---|
| 5 | False | False | |
| 0 | False | True | |
| 4 | True | True | |
| 1 | False | True | |
| 9 | False | False |
where clause in a query, you’re giving it a Pixeltable
expression, too (a boolean-valued one).
| image |
|---|
| image | label | rot_label |
|---|---|---|
| 5 | 4 |
& rather than and. Likewise, to form a logical
“or”, we’d use | rather than or:
and, or, and not to be
customized. There is one more instance of this to be aware of: to check
whether an expression is None, it’s necessary to write (say)
t.label is None, for the same reason.
Arrays
In addition to lists and dicts, Pixeltable also has built-in support for numerical arrays. A typical place where arrays show up is as the output of an embedding.| image | clip |
|---|---|
| [-0.052 -0.259 -0.481 -0.628 0.053 -0.032 ... 0.079 0.087 -0.346 0.904 0.037 -0.161] | |
| [ 0.063 -0.174 -0.263 -0.596 0.218 -0.332 ... 0.295 0.028 0.001 0.823 -0.149 -0.163] | |
| [-0.059 -0.222 -0.251 -0.503 -0.112 -0.233 ... 0.038 0.107 -0.329 0.765 -0.363 0.206] | |
| [ 0.098 -0.196 -0.42 -0.589 0.111 -0.107 ... 0.266 0.179 -0.143 1.023 -0.136 0.092] | |
| [-0.046 -0.147 -0.455 -0.576 0.228 -0.115 ... 0.521 0.086 -0.24 0.786 0.098 0.171] |
pxt.Array is an ordinary NumPy array
(np.ndarray), so that an array-typed column is a column of NumPy
arrays (in this example, representing the embedding output of each image
in the table). As with lists, arrays can be sliced in all the usual
ways.
| col_0 | col_1 | col_2 |
|---|---|---|
| -0.052 | [-0.032 0.07 1.117 -0.037 -0.158] | [ 0.904 0.037 -0.161] |
| 0.063 | [-0.332 -0.201 1.023 -0.003 -0.094] | [ 0.823 -0.149 -0.163] |
| -0.059 | [-0.233 0.238 0.864 -0.135 -0.017] | [ 0.765 -0.363 0.206] |
| 0.098 | [-0.107 0.038 1.081 -0.111 0.172] | [ 1.023 -0.136 0.092] |
| -0.046 | [-0.115 0.066 1.089 -0.017 0.105] | [0.786 0.098 0.171] |
Ad hoc UDFs with apply
We’ve now seen the most commonly encountered Pixeltable expression
types. There are a few other less commonly encountered expressions that
are occasionally useful.
You can use apply to map any Python function onto a column of data.
You can think of apply as a quick way of constructing an “on-the-fly”
UDF for one-off use.
applying doesn’t have type
hints (as in the example here), you’ll need to specify the output column
type explicitly.
Type Conversion with astype
Sometimes it’s useful to transform an expression of one type into a
different type. For example, you can use astype to turn an expression
of type pxt.Json into one of type pxt.String. This assumes that the
value being converted is actually a string; otherwise, you’ll get an
exception. Here’s an example:
Column Properties
SomeColumnRef expressions have additional useful properties. A media
column (image, video, audio, or document) has the following two
properties:
localpath: the media location on the local filesystemfileurl: the original URL where the media resides (could be the same aslocalpath)
| image | image_localpath |
|---|---|
| /Users/asiegel/.pixeltable/media/99266a70fa5141bc9b5d1c9fddcc7a16/92/9276/99266a70fa5141bc9b5d1c9fddcc7a16_0_1_927616cf87bf45ea9a3ebabd1da9bc01.jpeg | |
| /Users/asiegel/.pixeltable/media/99266a70fa5141bc9b5d1c9fddcc7a16/d3/d360/99266a70fa5141bc9b5d1c9fddcc7a16_0_1_d360c94ae7624066a723e7e1ccd4cf37.jpeg | |
| /Users/asiegel/.pixeltable/media/99266a70fa5141bc9b5d1c9fddcc7a16/4f/4f38/99266a70fa5141bc9b5d1c9fddcc7a16_0_1_4f38ed14b91445b2b03f1d4772e7f2e7.jpeg | |
| /Users/asiegel/.pixeltable/media/99266a70fa5141bc9b5d1c9fddcc7a16/0c/0ca1/99266a70fa5141bc9b5d1c9fddcc7a16_0_1_0ca1cf9c60cc4c90abfd0d3d4e6ef4c3.jpeg | |
| /Users/asiegel/.pixeltable/media/99266a70fa5141bc9b5d1c9fddcc7a16/65/657d/99266a70fa5141bc9b5d1c9fddcc7a16_0_1_657dcf92103f409a9203817b2565d05a.jpeg |
errortype and
errormsg. These properties will usually be None. However, if the
computed column was created with on_error='ignore' and an exception
was encountered during column execution, then the properties will
contain additional information about the exception.
To demonstrate this feature, we’re going to deliberately trigger an
exception in a computed column. The images in our example table are
black and white, meaning they have only one color channel. If we try to
extract a channel other than channel number 0, we’ll get an exception.
Ordinarily when we call add_computed_column, the exception is raised
and the add_computed_column operation is aborted.
on_error='ignore', the exception will be logged in the
column properties instead.
None values,
but the errortype and errormsg fields contain details of the error.
| image | channel | channel_errortype | channel_errormsg |
|---|---|---|---|
| None | ValueError | band index out of range | |
| None | ValueError | band index out of range | |
| None | ValueError | band index out of range | |
| None | ValueError | band index out of range | |
| None | ValueError | band index out of range |
The Pixeltable Type System
We’ve seen that every column and every expression in Pixeltable has an associated Pixeltable type. In this section, we’ll briefly survey the various Pixeltable types and their uses. Here are all the supported types and their corresponding Python types:| Pixeltable type | Python type |
|---|---|
| pxt.String | str |
| pxt.Int | int |
| pxt.Float | float |
| pxt.Bool | bool |
| pxt.Timestamp | datetime.datetime |
| pxt.Json | ** |
| pxt.Array | np.ndarray |
| pxt.Image | PIL.Image.Image |
| pxt.Video | str |
| pxt.Audio | str |
| pxt.Document | str |
pxt.Json, it can be any of str,
int, float, bool, list, or dict.
pxt.Audio, pxt.Video, and
pxt.Document all correspond to the Python type
str. This is because those types are represented by file
paths that reference the media in question. When you query for, say,
t.select(t.video_col), you’re guaranteed to get a file path
on the local filesystem (Pixeltable will download and cache a
local copy of the video if necessary to ensure this). If you want the
original URL, use t.video_col.fileurl instead.
Several types can be specialized to constrain the allowable data in
a column.
pxt.Imagecan be specialized with a resolution and/or an image mode:pxt.Image[(300,200)]- images with width 300 and height 200pxt.Image['RGB']- images with mode'RGB'; see the PIL Documentation for the full listpxt.Image[(300,200), 'RGB']- combines the above constraints
pxt.Arraycan be specialized with a shape and/or a dtype:pxt.Array[pxt.Float]- arrays with dtypepxt.Floatpxt.Array[(64,64,3), pxt.Float]- 3-dimensional arrays with dtypepxt.Floatand 64x64x3 shape
| table 'demo.mnist' |
| Column Name | Type | Computed With |
|---|---|---|
| image | Image | |
| label | String | |
| classification | Json | vit_for_image_classification(image, model_id='farleyknight-org-username/vit-base-mnist') |
| pred_label | Json | classification.label_text[0] |
| clip | Array[(512,), Float] | clip(image, model_id='openai/clip-vit-base-patch32') |
| channel | Image['L'] | image.getchannel(1) |
t.clip has type pxt.Array[(512,), pxt.Float], since the output of
the embedding is always a 1x512 array. t.channel has type
Image['L'], since it’s always an 'L' mode (1-channel) image.
You can freely use pxt.Image by itself to mean “any image,
without constraints”, but numerical arrays must always specify a shape
and a dtype; pxt.Array by itself will raise an error.
Array shapes follow standard numpy conventions: a shape is a tuple of
integers, such as (512,) or (64,64,3). A
None may be used in place of an integer to indicate an
unconstrained size for that dimension, as in (None,None,3)
(3-dimensional array with two unconstrained dimensions), or simply
(None,) (unconstrained 1-dimensional array).