Daffodil cheatsheet¶
Daffodil keeps a table as a list of rows, each a plain Python list, with the column names in a dict. Every line of code in the task tables is run as a test. The docs are at https://raylutz.github.io/daffodil/. This file is generated from notes/scripts/cheatsheet_content.py. Edit that file, not this one.
Setup¶
Each task table below starts from this code, with the default modes. The lines within one table run in order, so a line may use what the line above it made. The tables do not depend on each other. The SqlDaf table is one sequence, from top to bottom.
from daffodil.daf import Daf, NULL
from daffodil.keyedlist import KeyedList
daf = Daf(cols=['id', 'item', 'g', 'qty'], keyfield='id',
lol=[['a1', 'pen', 'x', 3], ['b2', 'ink', 'y', 10], ['c3', 'pad', 'x', 7], ['d4', 'cap', 'y', 1]])
other = Daf(cols=['id', 'price'], keyfield='id', lol=[['a1', 1.5], ['b2', 4.0]])
lod = [{'id': 'a1', 'qty': 3}, {'id': 'b2', 'qty': 10}]
types = {'id': str, 'item': str, 'g': str, 'qty': int}
k, n, row = 'a1', 2, ['e5', 'tag', 'x', 2]
open('data.csv', 'w').write('id,qty\na1,3\nb2,10\n')
Indexing at a glance¶
daf[rows, cols] rows and columns
daf[rows] rows, all columns
daf[:, cols] columns, all rows
| You write | As rows (first) | As columns (second) |
|---|---|---|
: |
all rows | all columns |
2 |
row 2, by position | column 2, by position |
-1 |
the last row | the last column |
2:5, 2:, :5, ::2 |
a slice of rows | a slice of columns |
range(2, 5) |
rows 2, 3, 4 | columns 2, 3, 4 |
[0, 4, 2] |
these rows, in this order | these columns, in this order |
[range(0, 2), range(4, 6)] |
rows 0, 1, 4, 5 | columns 0, 1, 4, 5 |
'a1' |
the row with key 'a1' |
the column named 'a1' |
['c3', 'a1'] |
the rows with these keys, in this order | the columns with these names, in this order |
('a1', 'c3') |
the rows from key a1 through key c3, in row order | the columns from a1 through c3, in column order |
('a1', None), ('a1',) |
key a1 to the end | name a1 to the end |
(None, 'c3') |
the start through key c3 | the start through name c3 |
[] |
no rows | no columns |
- Selecting whole rows, as
daf[rows]ordaf[rows, :], copies no data: the new Daf shares the rows with the original, so a change to a cell in one shows in the other. Selecting some of the columns makes new row lists. Usecopy('editable')for row lists of your own. - A list or dict held in a cell is the same object in every selection and in
copy('editable'). Onlycopy('deep')copies it. - An integer alone, or in a list, slice or range, is a position. A key or a column name that is an integer needs
select_krows()orselect_kcols(). - The two ends of a tuple are always keys or names, of any type, as in
daf[(10, 20), :]for int keys. The tuple takes the rows between them in the table's order. It does not compare the values. If the second end comes first in the table, the result is empty. - Rows by key need a keyfield.
- A tuple of row keys needs the column part, even if it is
:, as indaf[('a1', 'c3'), :]. A tuple of two standing alone is read as[rows, cols]. - A list holds integers, or ranges, or strings, not a mix.
Nonealone is not a selector. - The result is a Daf.
.to_value(),.to_list()or.to_dict()give plain values, or setdaf.retmode = 'val'. - The same selectors set values:
daf[rows, cols] = value.
Create¶
| Code | What it does |
|---|---|
daf = Daf(cols=['id', 'qty'], lol=[['a1', 3]], keyfield='id') |
from lists |
daf = Daf.from_lod(lod, keyfield='id') |
from a list of dicts |
daf = Daf.from_csv('data.csv') |
from a CSV file; the cells are text |
daf = Daf(cols=['id', 'qty']) |
empty, to append to |
Daf(schema=MySchema) takes the columns, dtypes, keyfield and defaults from a @schemaclass.
Look¶
| Code | What it does |
|---|---|
print(daf) |
a Markdown table |
len(daf) |
number of rows |
daf.shape() |
(rows, cols) |
daf.columns() |
column names |
daf.keys() |
keyfield values |
daf[:5] |
the first 5 rows |
Rows¶
| Code | What it does |
|---|---|
daf[2] |
row 2 |
daf[-3:] |
the last 3 rows |
daf[[0, 2]] |
rows 0 and 2 |
daf['a1'] |
the row with key 'a1' |
daf[['a1', 'c3']] |
the rows with these keys |
daf[('a1', 'c3'), :] |
keys a1 through c3; the : is needed |
daf[('c3', None), :] |
key c3 to the end |
Columns and cells¶
| Code | What it does |
|---|---|
daf[:, 'qty'] |
the qty column |
daf[:, ['id', 'qty']] |
two columns, in this order |
daf[:, 1:3] |
columns 1 and 2 |
daf[2, 'qty'] |
one cell, by position and name |
daf['b2', 'qty'] |
one cell, by key and name |
Values out of a selection¶
| Code | What it does |
|---|---|
daf['b2', 'qty'].to_value() |
the value: 10 |
daf[:, 'qty'].to_list() |
a column as a list |
daf['b2'].to_dict() |
a row as a dict |
daf.col('qty') |
a column as a list, without building a Daf |
daf.retmode = 'val' |
from now on, daf[...] of one cell, row or column gives values |
daf.retmode = 'obj' |
back to Dafs, the default |
Rows by condition¶
| Code | What it does |
|---|---|
daf.select_where(lambda row: row['qty'] > 5) |
rows where the function is true |
daf.select_by_dict({'g': 'x'}) |
rows equal to these values; much faster |
daf.select_by_dict([{'g': 'x'}, {'g': 'y'}]) |
rows equal to any of these |
daf.select_krows(['a1'], inverse=True) |
all rows but these keys |
Set values¶
| Code | What it does |
|---|---|
daf[2, 'qty'] = 0 |
one cell |
daf['b2', 'qty'] = 0 |
one cell, by key |
daf[:, 'qty'] = [1, 2, 3, 4] |
a column, from a list |
daf[:, 'qty'] = 0 |
a column, one value |
daf[0] = ['a1', 'pen', 'x', 5] |
a row, from a list |
daf[0] = {'id': 'a1', 'qty': 5} |
a row, by name; the columns it lacks become NULL |
daf.update_record_krow('b2', {'qty': 4}) |
some cells of the row with this key. An int is a key here |
daf.update_record_irow(1, {'qty': 4}) |
some cells of the row at this position |
Build in a loop¶
| Code | What it does |
|---|---|
daf.append({'id': 'e5', 'qty': 2}) |
a dict, placed by name |
daf.append(['e5', 'tag', 'x', 2]) |
a list, in column order |
daf.append(lol=[['e5', 'tag', 'x', 2]]) |
several lists |
row = daf.default_record(astype=KeyedList) |
a new row: the Daf's columns, with defaults |
row['qty'] = 2 |
fill it by name, in any order |
daf.append(row, fast=True) |
append it with no copy |
Without fast, every row is checked and copied.
With fast=True, a KeyedList must come from default_record(astype=KeyedList) of the same Daf. A dict or list is checked fully if it is the first row of an empty Daf, and otherwise only for its length. Nothing is copied, so do not change a list after appending it.
Reshape¶
| Code | What it does |
|---|---|
daf.sort_by_colname('qty', reverse=True) |
sort the rows of this Daf, in place |
daf.insert_col('note', ['', '', '', '']) |
add a column |
daf.select_cols(exclude_cols=['g']) |
drop columns |
daf.transpose() |
a new Daf, with rows as columns |
daf.copy('editable') |
a copy with its own rows |
daf.rename_cols({'qty': 'n'}) |
rename columns |
Types¶
| Code | What it does |
|---|---|
daf.apply_dtypes(dtypes=types) |
convert text to types, in place; names every column |
daf.apply_dtypes(dtypes={'qty': int}, cols=['qty']) |
convert only some columns |
NULL, a missing value, is '', the empty string. dtypes describe the columns, and do not force them. A cell can hold any Python object.
Apply, reduce, group¶
| Code | What it does |
|---|---|
daf.apply(lambda row: {'id': row['id'], 'q2': row['qty'] * 2}) |
a new table, row by row |
daf.reduce(Daf.sum_da, cols=['qty']) |
column sums, as a dict |
daf.groupby('g') |
a KeyedList of Dafs, one for each value |
daf.groupby_cols_reduce(['g'], Daf.sum_da, reduce_cols=['qty']) |
sums for each group |
daf.valuecounts_for_colname('g') |
counts: {'x': 2, 'y': 2} |
Joins and export¶
| Code | What it does |
|---|---|
daf.join(other, how='left') |
by key: inner, left, right or outer |
daf.to_lod() |
a list of dicts |
daf.to_md(max_rows=10) |
Markdown text |
daf.to_csv_file('out.csv') |
a CSV file |
daf.to_pandas_df() |
a pandas DataFrame |
Tables larger than memory¶
| Code | What it does |
|---|---|
from daffodil.sqldaf import SqlDaf |
SqlDaf keeps the rows in a SQLite table |
SqlDaf.db_path = 'job.db' |
the database file, set once |
sdaf = SqlDaf.from_daf(daf, name='items') |
a new table, items, from the Daf, with its keyfield |
sdaf = SqlDaf('items') |
open the table again by its name |
csv_sdaf = SqlDaf.from_csv('data.csv', keyfield='id') |
a new table, data, from a CSV file. Every cell is text |
csv_sdaf.apply_dtypes(dtypes={'qty': int}, cols='qty') |
convert numbers before any numeric work |
sdaf.select_record('a1') |
one row by key, as a dict |
sdaf.iloc(0) |
one row by position, as a dict |
sdaf['a1', 'qty'].to_value() |
one value. Brackets give a SqlDaf |
sdaf.update_record_krow('a1', {'qty': 4}) |
change some cells of the row with this key |
sdaf.select_where_sql('qty > ?', [5]) |
a selection by a SQL condition |
sdaf.select_by_dict({'g': 'x'}).to_daf() |
a selection, read into memory as a Daf |
sdaf.groupby('g') |
a KeyedList of selections, with an index on g |
sdaf.sql('SELECT g, SUM(qty) AS n FROM items GROUP BY g') |
any SQL. The whole result comes back as a Daf |
sdaf.copy(name='items_copy').save() |
an independent copy, kept in the file |
sdaf.sort_by_colname('qty') |
sort. The SqlDaf now refers to a sorted TEMP table |
SqlDaf.close() |
close the file. Every SqlDaf on it can no longer be used |
The lines run in order. They start from the Daf daf, with the columns id, item, g and qty and the keyfield id, and from the file data.csv, with the columns id and qty.
Brackets return a SqlDaf, even for one cell. Use .to_value() for a value, and select_record() or iloc() for a row.
An int in brackets is always a position when reading. Assigning with an int, as in sdaf[20, 'v'] = 1, raises NotImplementedError. Look up an int key with select_record(), and change some of its cells with update_record_krow().
A selection fixes its rows the first time to_daf(), len() or the like reads it whole. After that its rows are fixed, and changes to their values show in it. select_record() and iloc() with a position of 0 or more read one row, and fix nothing.
Setting a value through a selection changes the parent's row. append(), concat(), apply_dtypes() and a sort on a selection copy it first, and leave the parent as it was.
to_daf(), col(), keys(), to_lod() and sql() bring their whole result into memory. Select the rows first.
copy() and a sort make TEMP tables, which close() removes. save() keeps one in the file, under a name not in use.
Daf methods not written for SQL raise NotImplementedError. A Daf is faster for a table that fits in memory.
From pandas¶
| pandas | Daffodil |
|---|---|
df['qty'] |
daf[:, 'qty'] or daf.col('qty') |
df.loc[k] |
daf.select_record(k) |
df.iloc[n] |
daf[n] |
df.head(n) |
daf[:n] |
df.tail(n) |
daf[-n:] |
df[df.qty > 5] |
daf.select_where(lambda row: row['qty'] > 5) |
pd.concat([df, new]) |
daf.append(row) |
df.merge(other) |
daf.join(other) |
df.sort_values('qty') |
daf.copy().sort_by_colname('qty') |
df.T |
daf.transpose() |
Each line is the same task, not the same expression. daf[x] selects rows, where df[x] selects a column.
select_record(k) gives a dict. daf[k] gives a Daf, and there an int k is a position.
sort_by_colname() sorts the Daf itself, so copy first to keep the original order.
join() is one to one, on the keyfields. See the Joins page of the Guide.
Rules¶
- Import with
from daffodil.daf import Daf. Name a tabledaf, as pandas usesdf. - A Daf is
lol, a list of rows, each a plain list, plushd, a dict of column name to position. daf[rows, cols]selects. An integer is a position. A string is a key of the keyfield for rows, and a column name for columns. A tuple of two is an inclusive range, in row order, and its ends are keys of any type.- A selection returns a Daf. Use
.to_value(),.to_list()or.to_dict()for plain values, or setdaf.retmode = 'val'. - A selection of rows shares the rows with the original: changing a cell in it changes the original. A selection of columns makes new row lists.
copy()shares the row lists by default. Usecopy('editable')for row lists of your own, orcopy('deep')when a list or dict in a cell must be copied too.for row in dafgives each row as a new dict. Assigning to it does not change the table. Useiter_klist(),apply_in_place()orupdate_record_krow()to change values.append()does not check keys, so a key can repeat. A lookup of a repeated key finds its last row.- A write with
[]to a key cell does not update the key index. Calldaf.set_keyfield(daf.keyfield)after changing keys, before the next lookup. apply_dtypes()leaves text that does not convert as it is, with no error. Usecheck_schema()to find it.sort_by_colname()sorts the Daf itself.transpose()returns a new Daf.append(),apply_dtypes()and most changing methods return the Daf itself, changed in place.- A missing value is NULL, the empty string
'', not None or NaN. - The keyfield column stays in the table. Key lookups are dict lookups, built when first needed.
- Rows are cheap and columns cost more. Name the columns you need, as with
cols=, rather than dropping the others.
Common mistakes¶
daf[:-5]is all but the last 5 rows. The last 5 aredaf[-5:].apply_dtypes(dtypes=...)withoutcols=must name every column. To convert some, addcols=.groupby_cols_reduce()withoutreduce_cols=reduces nothing.- A KeyedList row is not a dict:
isinstance(row, dict)is False, andjson.dumps(row)fails. Userow.to_dict(). - With
append(row, fast=True)the table keeps the list itself. Do not fill one list in place and append it again. daf[('a1', 'c3')]without, :is read as[rows, cols]: row a1, column c3. If a column c3 exists, it returns that one cell, with no error. Otherwise it raises KeyError. Writedaf[('a1', 'c3'), :]for the range.- Methods that do not exist, from old examples:
add_idx()isinsert_idx_col(),to_csv()isto_csv_file().