Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

list_kurtosis_pop

Prev Next

Applies to VAST Cluster 5.5.

Functions › List, struct and map functions › Aggregate a list. Returns the population excess kurtosis of the elements of a numeric list: how heavy the tails are compared with a normal distribution, with no correction for the sample size. It needs at least two non-NULL elements that aren't all the same.

Syntax

list_kurtosis_pop(l)
Argument Type Description
l a list of numbers The values whose kurtosis is computed.

Returns: DOUBLE.

Set up the examples

The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_lists_list_kurtosis_pop. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.

This block connects, defines the run() and plan() helpers, and creates a table of five rows of lists: whole numbers (nums) and measurements (vals). Row 2 has a NULL element, row 3 has empty lists, row 4 has no values, and row 5's measurements are close to the largest DOUBLE:

import os
import re

import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb

BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_lists_list_kurtosis_pop"'

conn = dbapi.connect(
    driver=adbc_driver_vastdb.get_driver_path(),
    db_kwargs={
        "vast.db.endpoint": os.environ["QE_ENDPOINT"],
        "vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
        "vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
    },
    autocommit=True,
)

def server_message(e):
    """The server's message in a driver error, without the request ID or the echoed statement."""
    text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
    found = (re.search(r'message: "(.*?)", request_id', text, re.S)
             or re.search(r"description: (.*)\)", text, re.S))
    message = found.group(1) if found else text.strip().splitlines()[-1]
    message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
    message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
    return message.replace(BUCKET, "<bucket>")

def run(sql):
    """Run one statement. Print the rows it returns, or the error."""
    try:
        with conn.cursor() as cur:
            cur.execute(sql)
            rows = cur.fetchall() if cur.description else []
    except (adbc_driver_manager.Error, OSError) as e:
        print(f"{type(e).__name__}: {server_message(e)}")
    else:
        print(rows)

def plan(sql):
    """Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
    with conn.cursor() as cur:
        cur.execute("EXPLAIN " + sql)
        steps = cur.fetchall()[0][1]
    print("\n".join(line for line in steps.splitlines() if "Rows" not in line))

TABLE = f"{SCHEMA}.samples"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, nums BIGINT[], vals DOUBLE[])")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, [1, 2, 3], [0.5, 2.5, -1.0]), "
    "(2, [3, NULL, 3], [2.0, NULL]), "
    "(3, [], []), "
    "(4, NULL, NULL), "
    "(5, [-5, 0, -5, 10], [1e308, 1e308, -1e-308])")
[]
[]
[(5,)]

What it does

list_kurtosis_pop(l) treats the non-NULL elements of l as a whole population. It divides their fourth central moment by the square of the second and subtracts 3, so that a normal distribution gives about 0:

  • [1.0, 2.0] gives -2.0, the lowest value possible: two equally likely values, no tails.
  • [1.0, 2.0, 3.0, 10.0] gives -0.7696000000000001. The sample form, list_kurtosis, gives 3.227999999999999 for the same list: its correction for the sample size is large for short lists.
  • The calculation is in floating point, so a result can be a little off its exact value: [1.0, 2.0, 3.0] gives -1.4999999999999787, not -1.5.
  • A single element, or elements that are all the same, give NULL.

NULLs and edge cases

Input Result
NULL, or NULL::BIGINT[] NULL
[]::DOUBLE[] (empty list) NULL
[NULL, NULL]::DOUBLE[] NULL
[1.0] NULL
[5, 5, 5, 5] NULL
[1.0, 2.0] -2.0
[1.0, 2.0, 3.0] -1.4999999999999787
typeof(list_kurtosis_pop([1, 2])) 'DOUBLE'
A list holding NaN or infinity, or values that overflow ([1e308, -1e308, 1e308]) Error: Kurtosis is out of range!
A number, string, BLOB, date or boolean Error: No function matches the given name and argument types, for example 'list_aggr(INTEGER_LITERAL, STRING_LITERAL)'

The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:

run("SELECT list_kurtosis_pop(NULL), list_kurtosis_pop(NULL::BIGINT[]), list_kurtosis_pop([]::DOUBLE[]), "
    "list_kurtosis_pop([NULL, NULL]::DOUBLE[])")
run("SELECT list_kurtosis_pop([1.0]), list_kurtosis_pop([5, 5, 5, 5]), list_kurtosis_pop([1.0, 2.0]), "
    "list_kurtosis_pop([1.0, 2.0, 3.0]), typeof(list_kurtosis_pop([1, 2]))")
run("SELECT list_kurtosis_pop(['NaN'::DOUBLE, 1.0, 2.0])")
run("SELECT list_kurtosis_pop(['inf'::DOUBLE, 1.0, 2.0])")
run("SELECT list_kurtosis_pop([1e308, -1e308, 1e308])")
run(f"SELECT list_kurtosis_pop(123) FROM {TABLE} LIMIT 1")
[(None, None, None, None)]
[(None, None, -2.0, -1.4999999999999787, 'DOUBLE')]
OSError: Kurtosis is out of range!
OSError: Kurtosis is out of range!
OSError: Kurtosis is out of range!
InternalError: Binder Error: No function matches the given name and argument types 'list_aggr(INTEGER_LITERAL, STRING_LITERAL)'. You might need to add explicit type casts.

Examples

The kurtosis of each row's numbers. Row 2's [3, NULL, 3] has no spread, so its result is NULL, as are the empty list and the NULL list:

run(f"SELECT id, nums, list_kurtosis_pop(nums) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, [1, 2, 3], -1.4999999999999787), (2, [3, None, 3], None), (3, [], None), (4, None, None), (5, [-5, 0, -5, 10], -1.0)]

The rows whose numbers are spread more evenly than a normal distribution:

run(f"SELECT id, nums FROM {TABLE} WHERE list_kurtosis_pop(nums) < 0 ORDER BY id LIMIT 10")
[(1, [1, 2, 3]), (5, [-5, 0, -5, 10])]

How it runs in VAST

The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). The scan reads a list column whole, and list_kurtosis_pop() always runs in the Query Engine:

  • In the SELECT list, a projection step computes list_kurtosis_pop() for each row the scan returns.
  • In WHERE, the scan sends the list for every row, and a FILTER step computes list_kurtosis_pop() and keeps the rows that match. No rows are skipped in storage.

The plan of the WHERE example shows the FILTER step above the scan:

plan(f"SELECT id FROM {TABLE} WHERE list_kurtosis_pop(nums) < 0")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│  (list_aggr(CAST(nums AS  │
│  DOUBLE[]), 'kurtosis_pop'│
│          ) < 0.0)         │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│            nums           │
│             id            │
│                           │
└───────────────────────────┘

Make it faster

  • Add a condition on a plain column. A comparison of a column with a constant, such as id > 1, is evaluated in VAST DataBase, so list_kurtosis_pop() runs only on the rows that pass it. The plan for id > 1 alone has no FILTER step:
plan(f"SELECT id FROM {TABLE} WHERE id > 1")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
  • Store the kurtosis if you filter on it often. Compute it when you write the rows and keep it in its own column: kurtosis < 0 is pushed down, list_kurtosis_pop(nums) < 0 isn't.

Limitations and errors

  • NaN, infinity and overflow fail the whole query. If any row's list holds NaN or an infinite value, or values large enough to overflow a DOUBLE in the calculation, the query fails with Kurtosis is out of range! and returns no rows. Row 5's measurements (1e308 twice) are enough. Keep such values out of the lists you pass to list_kurtosis_pop:
run(f"SELECT id, list_kurtosis_pop(vals) FROM {TABLE} ORDER BY id LIMIT 10")
OSError: Kurtosis is out of range!
  • Lists only. Any other type fails with No function matches the given name and argument types. The error names list_aggr, the list aggregate that list_kurtosis_pop runs on, and its accepted form list_aggr(ANY[], VARCHAR, [ANY...]) -> ANY.
  • ORDER BY needs a LIMIT. Without one, the query fails with The operator 'ORDER_BY' type=1 is currently not supported for this usage.

Similar functions

Function What it computes Fewest elements Result for [1.0, 2.0, 3.0, 10.0]::DOUBLE[]
list_kurtosis_pop population excess kurtosis 2 -0.7696000000000001
list_kurtosis sample excess kurtosis, corrected for the sample size 4 3.227999999999999
list_skewness sample skewness: asymmetry 3 1.763632614803888
run("SELECT list_kurtosis_pop(l), list_kurtosis(l), list_skewness(l) FROM (SELECT [1.0, 2.0, 3.0, 10.0]::DOUBLE[] AS l)")
[(-0.7696000000000001, 3.227999999999999, 1.763632614803888)]

Clean up:

run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]

See also