Apply, reduce, group and join¶
Functions over rows, groups and counts, and joins.
Functions over rows¶
apply ¶
apply(func, by='row', keylist=None, **kwargs)
Apply a Python function to each row, and collect the results in a new table, as Daf.apply() does.
The function gets each row as a dict, with Python NULL for a missing value, and returns what to
add to the new table, by the rules of Daf.apply():
- A dict, or a KeyedList, adds one row.
- A list of them adds a row for each, so one row can become several.
- None, an empty dict or an empty list adds nothing, so the row is left out.
The new table takes its columns from the first row returned. A later row is placed by
column name. A column it lacks gets Python NULL, and a name the first row did not have is dropped.
So have the function return every column in every row. The new table has no keyfield
and no dtypes, as the new Daf of Daf.apply() has none.
The rows are read 10,000 at a time, in order, and the results are written as they come,
all in one transaction. So a table larger than memory can be converted, and memory does
not grow. If the function raises, that error is raised, as itself, and nothing is kept.
The new table is a TEMP table, as copy() makes. Keep it with save(name).
On 300,000 rows that each became 4, this took 23.1 s, most of it in the function. A registered SQL function cannot return a row, which is why the function runs in Python.
With by='table' the function gets this SqlDaf, and its result is returned as it is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Callable[..., Any]
|
The function. It takes a row and the keyword arguments, or this SqlDaf for |
required |
by
|
str
|
|
'row'
|
keylist
|
T_la | None
|
Not supported on a SqlDaf. Select the rows first. |
None
|
**kwargs
|
Any
|
Keyword arguments passed on to the function. |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The new table, or the result of the function for |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
|
TypeError
|
The function returns something other than a dict, a KeyedList, a list of them, or nothing. |
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> votes_sdaf = SqlDaf.from_daf(Daf(lol=[['b1', 'c0,c1'], ['b2', '']], cols=['ballot_id', 'contests']), name='votes')
>>> def split(row):
... return [{'ballot_id': row['ballot_id'], 'contest': c} for c in row['contests'].split(',') if c]
>>> votes_sdaf.apply(split).to_daf().lol
[['b1', 'c0'], ['b1', 'c1']]
reduce ¶
reduce(
func,
by="row",
cols=None,
initial_da=None,
indirect_col="",
silent_error=False,
**kwargs,
)
Combine all the rows into one result, using a function, as Daf.reduce() does.
The function is called in Python for each row, with the same arguments and the same
result as in Daf.reduce(). A sum or a count is a reduction, as with Daf.sum_da.
The rows are read 10,000 at a time, in order, so the table is never all in memory.
Memory use still depends on the result the function builds.
- With
by='row', each row is a KeyedList of its values. Unlike in a Daf, writing to it does not change the table. - With
by='sparse_row', the function gets the dict held inindirect_col, JSON or PYON text decoded as in a Daf, and the result starts asinitial_daor an empty dict. - With
by='table', the function gets this SqlDaf andcols.
For a plain total of columns, sum() is faster: SQLite adds them, and no row comes
into Python. by='col' is not supported, since it needs each whole column in memory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Callable[..., Any]
|
The reduction function, such as |
required |
by
|
str
|
|
'row'
|
cols
|
T_ls | None
|
The columns included in the reduction. All columns if None. |
None
|
initial_da
|
T_da | None
|
The result to start from, instead of zeros. |
None
|
indirect_col
|
str
|
A column that holds a dict. Required for |
''
|
silent_error
|
bool
|
If True, a row for which the function raises is skipped. |
False
|
**kwargs
|
Any
|
Keyword arguments passed on to the function. |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
A dict for |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
NotImplementedError
|
|
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> votes_sdaf = SqlDaf('votes', cols=['ballot_id', 'v', 'concise_votes'],
... lol=[['b1', 1, "{'c0b': 1, 'c0v': 1}"], ['b2', 2, '{"c0b": 1, "c1b": 1}']])
>>> votes_sdaf.reduce(Daf.sum_da, cols=['v'])
{'ballot_id': '', 'v': 3, 'concise_votes': ''}
>>> votes_sdaf.reduce(Daf.sum_da, by='sparse_row', indirect_col='concise_votes')
{'c0b': 2, 'c0v': 1, 'c1b': 1}
Groups¶
groupby ¶
groupby(
colname="", colnames=None, omit_nulls=False, cols=None
)
Group the rows by the values of a column, as Daf.groupby() does.
The result is a KeyedList of selections, one for each value, in the order first seen. It
works as a dict, as the result of Daf.groupby() does. One query finds the values. Each
group is then select_by_dict() of its value, so Python NULL is a value like any other. Nothing
is read until a group is read.
The first groupby on a column makes an index on it, in the parent table, unless an index
already starts with it. Each group is then a lookup. Without the index, reading each
group would need a full scan of the table. On 100,000 rows in 500 groups, summing every group took 2.3 s, against
14.8 s without it. The index stays with the table, and drop_cols() or rename_cols()
of the column drops it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colname
|
str
|
The column to group by. |
''
|
colnames
|
T_ls | None
|
Several columns to group by. See |
None
|
omit_nulls
|
bool
|
If True, leave out the rows with Python NULL in the column. |
False
|
cols
|
T_ls | None
|
Not supported on a SqlDaf yet. |
None
|
Returns:
| Type | Description |
|---|---|
KeyedList
|
A KeyedList that maps each value to a selection. |
Raises:
| Type | Description |
|---|---|
KeyError
|
The column is not a column. |
NotImplementedError
|
|
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> bif_sdaf = SqlDaf.from_daf(Daf(lol=[['b1', 'P1'], ['b2', 'P2'], ['b3', 'P1']], cols=['ballot_id', 'precinct']), name='bif')
>>> groups = bif_sdaf.groupby('precinct')
>>> {key: group.to_daf().lol for key, group in groups.items()}
{'P1': [['b1', 'P1'], ['b3', 'P1']], 'P2': [['b2', 'P2']]}
groupby_cols ¶
groupby_cols(colnames, cols=None, omit_nulls=False)
Group the rows by the values of several columns, as Daf.groupby_cols() does.
Each key is a tuple of the values, even for one column. Each group is a selection, as in
groupby().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colnames
|
T_ls
|
The columns to group by. |
required |
cols
|
T_ls | None
|
Not supported on a SqlDaf yet. |
None
|
omit_nulls
|
bool
|
If True, leave out the rows with Python NULL in any of the columns. |
False
|
Returns:
| Type | Description |
|---|---|
KeyedList
|
A KeyedList that maps each tuple of values to a selection. |
Raises:
| Type | Description |
|---|---|
KeyError
|
A name in |
NotImplementedError
|
|
group_where ¶
group_where(where, *, indirect_col=None, cols=None)
Group the rows by the keys a Python function returns, as Daf.group_where() does.
The function gets each row as a KeyedList, as in select_where(), and returns a key, a
list of keys, or None. A row goes into the group of each key, so a row can be in any
number of groups, and None leaves it out. As in Daf.group_where(), any iterable other
than a str is a list of keys, a tuple too. So to use a tuple as one key, return a list
of tuples.
The function runs once for each row, in one pass, and each (key, row) pair is written to a TEMP table. Each group is then a selection of the rows listed under its key, in the order of this SqlDaf. Nothing more is read until a group is read. The result is a KeyedList of selections, in the order the keys were first returned.
Name the columns the function reads in cols, as in select_where().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
where
|
Callable[[Any], Any]
|
A function of a row: a key, a list of keys, or None. |
required |
indirect_col
|
str | None
|
Not supported on a SqlDaf yet. |
None
|
cols
|
T_ls | None
|
The columns the function reads. All if None. |
None
|
Returns:
| Type | Description |
|---|---|
KeyedList
|
A KeyedList that maps each key to a selection. |
Raises:
| Type | Description |
|---|---|
KeyError
|
A name in |
NotImplementedError
|
|
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> bif_sdaf = SqlDaf.from_daf(Daf(lol=[['b1', 'P1'], ['b2', 'P2'], ['b3', 'P1']], cols=['ballot_id', 'precinct']), name='bif')
>>> groups = bif_sdaf.group_where(lambda row: [row['precinct'], 'all'], cols=['precinct'])
>>> {key: len(group) for key, group in groups.items()}
{'P1': 2, 'all': 3, 'P2': 1}
groupsum_daf ¶
groupsum_daf(colname, by='row', reduce_cols=None)
Group by a column and add up the other columns of each group, as Daf.groupsum_daf() does.
One GROUP BY does it all in SQLite. The result is a Daf in memory, with one row for each
value, in the order first seen, and colname as its keyfield. As in a
Daf, only numbers are added: text, even text of digits, counts as 0, and a column with
no numbers in a group totals 0. A column not in reduce_cols is Python NULL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colname
|
str
|
The column to group by. |
required |
by
|
str
|
Only |
'row'
|
reduce_cols
|
T_ls | None
|
The columns to add. All but |
None
|
Returns:
| Type | Description |
|---|---|
Daf
|
A Daf in memory, with one row for each group. |
Raises:
| Type | Description |
|---|---|
KeyError
|
|
NotImplementedError
|
|
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> nums_sdaf = SqlDaf.from_daf(Daf(lol=[['a', 1, 10], ['b', 2, 20], ['a', 3, 30]], cols=['g', 'x', 'y']), name='nums')
>>> nums_sdaf.groupsum_daf('g', reduce_cols=['y']).lol
[['a', '', 40], ['b', '', 20]]
sum ¶
sum(colnames_ls=None, numeric_only=False)
Total the columns, as Daf.sum() does, and return a dict of the totals.
Numbers are totalled by SQLite. A column that also holds text has its text cells read
into Python and added as Daf.sum() adds them: text that is not a number raises
ValueError, or counts as 0 with numeric_only. Python NULL is skipped. Each total starts as a
float, and is converted to the dtype of its column if there is one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colnames_ls
|
T_ls | None
|
The columns to total. All if None. |
None
|
numeric_only
|
bool
|
If True, total only the columns with an int or float dtype, and count text that is not a number as 0. |
False
|
Returns:
| Type | Description |
|---|---|
T_da
|
A dict that maps each column name to its total. |
Raises:
| Type | Description |
|---|---|
ValueError
|
A text cell cannot be converted to a number. |
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> SqlDaf.from_daf(Daf(lol=[[1, 10], [2, 20]], cols=['x', 'y']), name='nums').sum()
{'x': 3.0, 'y': 30.0}
Counts¶
valuecounts_for_colname ¶
valuecounts_for_colname(
colname, sort=False, reverse=True, omit_nulls=False
)
Count how often each value occurs in one column, as Daf.valuecounts_for_colname() does.
One GROUP BY counts them in SQLite, so no column is read into Python. The values are in
the order first seen, or by count with sort. Python NULL is counted as Python NULL, unless
omit_nulls. A column that does not exist gives an empty dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colname
|
str
|
The column to count. |
required |
sort
|
bool
|
If True, order by count. |
False
|
reverse
|
bool
|
With |
True
|
omit_nulls
|
bool
|
If True, leave out the count of Python NULL. |
False
|
Returns:
| Type | Description |
|---|---|
T_di
|
A dict that maps each value to its count. |
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> bif_sdaf = SqlDaf.from_daf(Daf(lol=[['b1', 'P1'], ['b2', 'P2'], ['b3', 'P1']], cols=['ballot_id', 'precinct']), name='bif')
>>> bif_sdaf.valuecounts_for_colname('precinct', sort=True)
{'P1': 2, 'P2': 1}
valuecounts_for_colnames_ls ¶
valuecounts_for_colnames_ls(
colnames_ls=None,
sort=False,
reverse=True,
omit_nulls=False,
)
Count how often each value occurs, in each of several columns, as Daf.valuecounts_for_colnames_ls() does.
It is one GROUP BY for each column. See valuecounts_for_colname().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colnames_ls
|
T_ls | None
|
The columns to count. All if None. |
None
|
sort
|
bool
|
If True, order each count by size. |
False
|
reverse
|
bool
|
With |
True
|
omit_nulls
|
bool
|
If True, leave out the count of Python NULL. |
False
|
Returns:
| Type | Description |
|---|---|
Dict[str, T_di]
|
A dict that maps each column to a dict of value and count. |
valuecounts_for_colname_selectedby_colname ¶
valuecounts_for_colname_selectedby_colname(
colname,
selectedby_colname,
selectedby_colvalue,
sort=False,
reverse=True,
)
Count the values of a column, in the rows where another column has a value, as the Daf method does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colname
|
str
|
The column to count. |
required |
selectedby_colname
|
str
|
The column to test. |
required |
selectedby_colvalue
|
Any
|
Only rows where that column has this value are counted. Python NULL matches Python NULL. |
required |
sort
|
bool
|
If True, order the counts by size. |
False
|
reverse
|
bool
|
With |
True
|
Returns:
| Type | Description |
|---|---|
T_di
|
A dict that maps each value to its count. It is empty if a column does not exist. |
valuecounts_for_colnames_ls_selectedby_colname ¶
valuecounts_for_colnames_ls_selectedby_colname(
colnames_ls=None,
selectedby_colname="",
selectedby_colvalue="",
sort=False,
reverse=True,
)
Count the values of several columns, in the rows where another column has a value, as the Daf method does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colnames_ls
|
T_ls | None
|
The columns to count. All if None. |
None
|
selectedby_colname
|
str
|
The column to test. |
''
|
selectedby_colvalue
|
Any
|
Only rows where that column has this value are counted. |
''
|
sort
|
bool
|
If True, order the counts by size. |
False
|
reverse
|
bool
|
With |
True
|
Returns:
| Type | Description |
|---|---|
Dict[str, T_di]
|
A dict that maps each column to a dict of value and count. |
valuecounts_for_colname1_groupedby_colname2 ¶
valuecounts_for_colname1_groupedby_colname2(
colname1, groupedby_colname2, sort=False, reverse=True
)
Count the values of one column, for each value of another, as the Daf method does.
One GROUP BY of the two columns counts them all. The groups, and the values in each, are in the order first seen. Use it to see whether two columns relate one to one: each group should then hold a single value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colname1
|
str
|
The column whose values are counted. |
required |
groupedby_colname2
|
str
|
The column whose values form the groups. |
required |
sort
|
bool
|
If True, order each count by size. |
False
|
reverse
|
bool
|
With |
True
|
Returns:
| Type | Description |
|---|---|
Dict[Any, T_di]
|
A dict that maps each value of the second column to a dict of value and count. |
value_counts_daf ¶
value_counts_daf(
colname,
sort=False,
reverse=True,
include_total=False,
omit_nulls=False,
)
Count the values of a column into a Daf in memory, with one row per value, as Daf.value_counts_daf() does.
The counts come from valuecounts_for_colname(). The Daf has the columns colname and
counts, and a row for each value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
colname
|
str
|
The column to count. |
required |
sort
|
bool
|
If True, order by count. |
False
|
reverse
|
bool
|
With |
True
|
include_total
|
bool
|
If True, add a row with the total. |
False
|
omit_nulls
|
bool
|
If True, leave out the count of Python NULL. |
False
|
Returns:
| Type | Description |
|---|---|
Daf
|
A Daf in memory, with a row for each value. |
daf_valuecount ¶
daf_valuecount(by='row', cols=None)
Count how often each value occurs, in each column, as Daf.daf_valuecount() does.
It gives the same result: a column that is not in cols gets Python NULL in place of its
counts, and a table with no rows gives an empty dict. Each column is one GROUP BY.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
by
|
str
|
Only |
'row'
|
cols
|
T_ls | None
|
The columns to count. All if None. |
None
|
Returns:
| Type | Description |
|---|---|
T_da
|
A dict that maps each column to a dict of value and count. |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
|
Joins¶
join ¶
join(
other_daf,
how="inner",
shared_fields=None,
tag_other=False,
custom_translator_daf=None,
diagnose=False,
name="",
fill=NULL,
)
Join two SqlDaf tables on their keyfields, as Daf.join() does, in SQL.
The columns are named exactly as Daf.join() names them, by the same translator: a
column that both have gets the name of its source as a suffix, such as name_audit,
unless it is in shared_fields, and tag_other suffixes every column of the other. The
names are those of the parent tables, or daf1 and daf2 if they are the same. The rows
come as in Daf.join(): in this table's order, and for right and outer, the other
table's unmatched rows after, in its order. A cell with no match is Python NULL. The keyfield of
the result is this table's.
The keyfields must be unique, as in Daf.join(). A repeated key is not checked, and the
result is then undefined. Check the keys first if they may repeat. A Python NULL key is
undefined too: see SqlDaf.
A row found only in the other table, in a right or outer join, takes its key and its
shared fields from the other table, as in Daf.join().
Nothing runs yet. The join is resolved the first time it is read, into a TEMP table of
new rows, as Daf.join() makes new rows. A selection made from it resolves it first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other_daf
|
SqlDaf
|
The SqlDaf to join with, in the same database file. |
required |
how
|
str
|
|
'inner'
|
shared_fields
|
T_ls | None
|
Columns that both have and that appear only once. |
None
|
tag_other
|
bool
|
If True, every column of the other, except shared ones, gets a suffix. |
False
|
custom_translator_daf
|
Daf | None
|
Not supported on a SqlDaf yet. |
None
|
diagnose
|
bool
|
Not used. |
False
|
name
|
str
|
The name of the result table. Generated if empty. |
''
|
fill
|
Any
|
The value for cells that have no match. Only Python NULL is supported. |
NULL
|
Returns:
| Type | Description |
|---|---|
SqlDaf
|
A SqlDaf of the joined rows, resolved when first read. |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
NotImplementedError
|
|
Examples:
>>> import tempfile
>>> SqlDaf.db_path = tempfile.mkdtemp() + '/job.db'
>>> a_sdaf = SqlDaf.from_daf(Daf(lol=[[1, 'Alice'], [2, 'Bob']], cols=['id', 'name'], keyfield='id'), name='a')
>>> b_sdaf = SqlDaf.from_daf(Daf(lol=[[1, 50], [3, 70]], cols=['id', 'salary'], keyfield='id'), name='b')
>>> a_sdaf.join(b_sdaf, how='left').to_daf().lol
[[1, 'Alice', 50], [2, 'Bob', '']]