Apply, reduce and formulas¶
Rows, not columns¶
Daffodil keeps a table as rows, where pandas and Polars keep columns. Appending rows and going through them one at a time suit that storage. Selecting whole rows reuses their row lists, and copies no cells. Inserting or deleting a row in the middle moves the rows after it in the outer list, so it costs more in a large table.
Working with columns costs more, as each column is spread across all the rows. Most of the
time you do not need to: rather than dropping the columns you do not want, name the ones you
do. Give cols= to reduce(). In an apply() function, read only the fields you need. To
show some columns, select them before to_md(). For heavy math over whole numeric columns,
hand them to NumPy or pandas, with to_numpy() or to_pandas_df().
apply(): a new table, row by row¶
apply() calls a function on each row, and makes a new table of what it returns. The row is a
dict, and the function returns a dict:
>>> from daffodil.daf import Daf
>>> daf = Daf(cols=['g', 'a', 'b'], lol=[['x', 1, 2], ['y', 3, ''], ['x', 5, 6]])
>>> doubled = daf.apply(lambda row: {'g': row['g'], 'a2': row['a'] * 2})
>>> doubled.columns(), doubled.lol
(['g', 'a2'], [['x', 2], ['y', 6], ['x', 10]])
apply_in_place() changes the rows of the table instead of making a new one.
reduce(): one record from many rows¶
reduce() folds the rows into one record with a function. Daf.sum_da adds up each column:
>>> daf.reduce(Daf.sum_da, cols=['a', 'b'])
{'g': '', 'a': 9, 'b': 8}
>>> daf.sum(['a', 'b'])
{'a': 9.0, 'b': 8.0}
NULL cells are skipped, so the missing b of the second row adds nothing.
sum() and reduce(Daf.sum_da) differ when a column holds text. sum() converts text
that is a number, such as '2', and raises ValueError for other text. Daf.sum_da skips
every value that is not a number, and never raises:
>>> mixed = Daf(cols=['n'], lol=[[1], ['2'], [''], [3.5]])
>>> mixed.sum(['n']), mixed.reduce(Daf.sum_da, cols=['n'])
({'n': 6.5}, {'n': 4.5})
Your own reduction¶
The function gets each row, the result so far, and the columns. It returns the new result.
The row is a KeyedList, which reads like a dict. The result has one value for each column,
and starts at 0 unless you give initial_da. This one counts the cells that are not NULL:
>>> from daffodil.daf import NULL
>>> def count_filled(row, result, cols):
... for col in cols:
... if row[col] is not NULL:
... result[col] += 1
... return result
>>> daf.reduce(count_filled, cols=['a', 'b'])
{'g': '', 'a': 3, 'b': 2}
A key of the result that is not a column is dropped. For a result of another shape, write a loop over the rows.
Groups¶
groupby() makes a Daf for each value of a column. groupsum_daf() adds up the other columns
of each group. groupby_cols_reduce() groups by several columns and reduces each group with
any function.
>>> {key: group.lol for key, group in daf.groupby('g').items()}
{'x': [['x', 1, 2], ['x', 5, 6]], 'y': [['y', 3, '']]}
>>> daf.groupsum_daf('g', reduce_cols=['a', 'b']).lol
[['x', 6, 8], ['y', 3, 0]]
>>> daf.groupby_cols_reduce(['g'], Daf.sum_da, reduce_cols=['a']).lol
[['x', 6], ['y', 3]]
>>> daf.valuecounts_for_colname('g')
{'x': 2, 'y': 1}
Data in many files¶
A table can describe a set of files, one row for each chunk of the data. manifest_apply()
applies a function to every row of every chunk, and manifest_reduce() reduces them all to
one record. Each chunk is read in turn, so the whole data never has to fit in memory, and the
chunks can be shared out to run in parallel.
Spreadsheet-like formulas¶
apply_formulas() fills cells from a second Daf of the same shape that holds formulas, as a
spreadsheet does. A formula is Python, evaluated for its cell. An empty formula leaves its
cell alone. In a formula:
$dis the Daf.$ris the row of the cell, and$cits column.
So a formula is absolute unless it uses $r or $c, the reverse of a spreadsheet. The
formulas are evaluated again until no value changes. If the values still change after 100
passes, it raises RuntimeError. This is a limit on the passes, not a check for circular
formulas. A circle that settles raises nothing.
The formulas run with Python's eval(), so use only formulas you trust. If a formula raises,
the cells already changed stay changed.
This sums each row into the last column, and each column into the last row:
>>> data = Daf(cols=['A', 'B', 'C'], lol=[[1, 2, 0], [4, 5, 0], [7, 8, 0], [0, 0, 0]])
>>> row_sum, col_sum = "sum($d[$r, :$c])", "sum($d[:$r, $c])"
>>> formulas = Daf(cols=['A', 'B', 'C'], lol=[['', '', row_sum],
... ['', '', row_sum],
... ['', '', row_sum],
... [col_sum, col_sum, col_sum]])
>>> _ = data.apply_formulas(formulas)
>>> data.lol
[[1, 2, 3], [4, 5, 9], [7, 8, 15], [12, 15, 27]]
The formulas themselves are a table, so they can be written in a spreadsheet, saved as CSV, and
read with Daf.from_csv().