Filtering
Once you've selected rows (see Row Selection), press " to open a new filtered sheet containing only those rows. This two-step model — select, then filter — is non-destructive: your source data never changes.
Selection and filtering are two steps: (1) select rows with |, z|, or , — (2) open filtered sheet with ". This is safer than Excel's AutoFilter because the source sheet is never modified.
Open a Filtered Sheet (")
# 1. Select rows
| # move to column, press |, enter regex
# or
z| # select by Python expression
# or
, # select by current cell value
# 2. Open filtered sheet
"
| Key | Contents | Linked to source? |
|---|---|---|
" | Selected rows only | Yes — reflects source changes |
g" | All rows (no filter) | Yes — useful for working copy |
gz" | Selected rows, deep copied | No — fully independent |
Save the Filtered View
# After opening filtered sheet with "
Ctrl+S
# Save to file — only filtered rows are written
Filter by Regex (|)
Select rows where the current column matches a pattern.
| regex # select rows matching regex in current column
g| regex # select rows matching regex in ANY visible column
\ regex # unselect matching rows
Common patterns:
| Goal | Regex | Command |
|---|---|---|
| Contains 'error' | error | | error |
| Starts with 'SG' | ^SG | | ^SG |
| Ends with '.com' | \.com$ | | \.com$ |
| Exact match | ^active$ | | ^active$ |
| Contains digits | \d+ | | \d+ |
| Either X or Y | ^X$|^Y$ | | ^X$|^Y$ |
vd ~/github/practice-folder/visidata/01-loading/01-employees.csv
# Filter employees in Engineering
# Move to 'department' column
|
# Enter: Engineering
# → Engineering rows highlighted yellow
# Open filtered sheet
"
# → new sheet with only Engineering employees
# Save to CSV
Ctrl+S
# Enter: /tmp/engineering.csv
Filter by Python Expression (z|)
Select rows matching a Python condition. Column names are variables.
z| expr # select rows matching Python expression
z\ expr # unselect matching rows
# Filter: salary above 80000
z|
# Enter: salary > 80000
"
# Filter: engineering employees with high salary
z|
# Enter: department == "Engineering" and salary > 85000
"
# Filter: empty/null values
z|
# Enter: region is None
"
# Filter: text contains
z|
# Enter: "web" in hostname
Filter by Value Match (,)
Quick-select all rows sharing the current cell's value — no typing.
, # select all rows with same display value as current cell
z, # select by stored (not display) value
g, # select rows matching current row across ALL columns
# Move cursor to a cell containing 'sg'
,
# → all rows where current column = 'sg' are selected
"
# → filtered sheet with only sg rows
Multi-Condition Filtering
Equivalent to Excel's =FILTER(range, (cond1)*(cond2)*(cond3)):
# Excel: =FILTER(orders, (region="APAC")*(amount>1000)*(status="shipped"))
# VisiData:
z|
# Enter: region == 'APAC' and amount > 1000 and status == 'shipped'
"
Filter Friendly — gf
Natural-language column filter with is, is not, contains, starts with, ends with conditions and multi-value and/or/comma support. Press g then f to activate.
gf # prompt: filter friendly: <column> <op> <value> [and/or ...]
| Condition | Example |
|---|---|
is | status is pending,completed |
is not | status is not shipped |
contains | email contains gmail |
starts with | name starts with A |
ends with | email ends with .com |
> / greater than / above | amount > 500 |
< / less than / below | amount < 100 |
>= / at least / gte | salary at least 50000 |
<= / at most / lte | salary at most 100000 |
vd ~/github/practice-folder/visidata/01-loading/01-employees.csv
gf
# Enter: department is Engineering
# → only rows where department = "Engineering"
gf
# Enter: salary greater than 80000 and status is pending
# → Engineering rows with salary > 80000 and pending status
Uses the same restore mechanism as fk/fx/fclear — filter in-place, add rows, clear to restore. The ff longname is also registered, but g f is the reliable primary shortcut in VisiData 3.0.2 because Space opens the command palette rather than acting as a prefix key.
Filter the Column Under the Cursor — z Shift+C
When the cursor is already on the column you want to filter, you do not have to
retype the column name. Press z then Shift+C and type only the condition and
value.
# Move the cursor to the 'status' column first
z Shift+C # prompt: filter 'status': is not done
This is the cursor-scoped form of gf: the column comes from the cursor, so
is not done replaces the full status is not done. It is also available as
Space fc Enter (longname: filter-friendly-current).
Full form (gf) | Cursor form (z Shift+C) |
|---|---|
department is Engineering | cursor on department, then is Engineering |
status is not done | cursor on status, then is not done |
salary greater than 80000 | cursor on salary, then greater than 80000 |
vd ~/github/practice-folder/visidata/01-loading/01-employees.csv
# Move the cursor to the 'status' column
z Shift+C
# Enter: is not done
# → only rows whose status is not "done"
gc
# → previous row view restored
z Shift+C was chosen because g Shift+F is VisiData's built-in freq-keys
(frequency of selected keys). Restore the previous view with gc / Space fclear,
the same as gf. Rows added while a friendly filter is active are kept in the
master row list and remain available after clearing the filter.
Saving While a Filter Is Active (Custom — View-Only)
The custom in-place filters (gf, g r, z Shift+C, Space fk, Space fx) are
view-only for saving. When you press Ctrl+S while such a filter is active,
VisiData writes the full dataset, not only the visible rows. This stops an
accidental save — for example a reflexive Ctrl+S after gf — from deleting the
hidden rows from the file. The same protection covers zq (force save) and
headless batch -o.
g r is covered because it replays a stored g f expression through the same
in-place filter path, so the master row list is tracked exactly as with a typed
g f. The same is true for z Shift+C, Space fk, and Space fx.
gf
# Enter: status is pending # only pending rows are visible
Ctrl+S
# Enter path: /tmp/all_rows.csv
# → the saved file contains every row, not just the pending ones
Press gc first when you actually want a filtered export. This is a custom
common-config feature (features/save-all-rows/), not stock VisiData behavior,
and it does not apply to the built-in " filtered sheet — that sheet still saves
only the rows it contains.
Rows you delete on purpose while a custom filter is active are not tracked in the
master list, so they reappear when you save. Run gc before deleting if you want
the deletion to persist. The gD reminder toggle is also not covered by this
safety net because it keeps its own separate snapshot.
This protection is local to VisiData. If your file is watched by an external sync — for example a Notion-to-CSV reconcile that deletes rows missing from an external database — a save can still be changed afterward by that pipeline. The view-only save only guarantees what VisiData itself writes.
Filter History — g r / g R (Custom)
Long friendly-filter expressions do not need to be typed repeatedly. After a
successful g f or z Shift+C filter, the expression is added to persistent
history. Press g then r to open an fzf picker and reuse one of the latest 20
unique expressions.
g r
# Type: department is not Sales
# Press Enter to replay the selected expression on the current sheet
The history picker supports normal fzf searching. Press Esc to cancel without
changing the current sheet. Cancelled filters, invalid expressions, and filters
that reference a missing column are not saved. Reusing a command moves it to the
top of the history rather than creating a duplicate.
Press g then R (Shift+R) to open the same picker in delete mode.
Selecting an entry removes it from saved history; it is not applied and the
current sheet is not changed.
g Shift+R
# Press Enter to delete the highlighted filter from history
g r and g R are custom commands defined in the shared common-config, not
built-in VisiData bindings, so they only work while this config is loaded.
History is stored locally at:
~/.local/state/visidata/filter-friendly-history
Set VISIDATA_FILTER_HISTORY to use another path, for example in a test
environment. History is applied to the current sheet, so the saved expression's
column names must exist in that sheet.
The implementation is part of the shared common-config feature layout:
common-config/visidata/features/filter-friendly/
common-config/visidata/features/filter-friendly-history/
common-config/visidata/.visidatarc.d/Keybindings/
The feature directories contain source and documentation; the .visidatarc.d
directory contains the active loader symlinks.
Keyword Filter (Custom — fk)
This is a custom command defined in 35-filter.py under the common-config repository. Unlike the default filter methods that require selection first, fk directly prompts for keywords and filters in-place.
Space fk Enter # prompt: filter keywords (comma-separated): done,pending
Space fclear Enter # restore the previous row view
Type comma-separated keywords. Rows where any visible column contains any keyword are kept; all others are hidden. The filter is an OR across keywords and columns.
vd ~/github/practice-folder/visidata/04-cleaning/sales.csv
Space
# Type: fk
# Enter: pending,urgent
# → rows where any column contains "pending" OR "urgent"
Space
# Type: fclear
# Enter
# → previous row view restored
Expression Filter (Custom — fx)
Filter rows in-place using a Python expression. Column names are available as variables.
Space fx Enter # prompt: filter expression: amount > 500
Space fclear Enter # restore the previous row view
Only rows where the expression evaluates to True are kept. Same Python syntax as z| but hides non-matching rows directly instead of marking them.
vd ~/github/practice-folder/visidata/01-loading/01-employees.csv
Space
# Type: fx
# Enter: salary > 70000
# → only rows where salary > 70000 remain visible
Space
# Type: fclear
# Enter
# → previous row view restored
Friendly Filter (Custom — ff)
A natural-language filter that works like fx but reads as plain English. Supports cross-column and/or conditions and English operator aliases.
Space ff Enter # prompt: filter friendly: department contains Engineering
Space fclear Enter # restore previous row view
Operators
| Operator | Aliases | Example |
|---|---|---|
is | same, equals, equal, equal to | status is pending |
is not | status is not done | |
contains | has, includes, with | email has gmail |
starts with | begins with, prefix | name begins with A |
ends with | suffix | email suffix .com |
> | greater than, gt, more than, above | salary > 80000 |
< | less than, lt, fewer than, below | amount < 100 |
>= | greater or equal, gte, at least, min | salary at least 50000 |
<= | less or equal, lte, at most, max | salary at most 100000 |
= | (same as is) | salary = 80000 |
Cross-column Conditions
Use and/or to combine conditions across different columns — no parentheses needed:
Space ff → department contains Engineering and salary > 80000
Space ff → name starts with A or department is Sales
Space ff → salary >= 50000 and salary <= 100000
Same-column Multi-value
Comma-separated values are OR-joined within a single column:
Space ff → department contains Engineering or Sales
# → rows where department has "Engineering" OR "Sales"
The same fclear command restores the previous view — shared with fk and fx. A keyboard shortcut gc is also available.
Space ff → status is pending and priority at least high
gc
Practice
vd ~/github/practice-folder/visidata/01-loading/01-employees.csv
Space ff → department contains Engineering and salary > 80000
# → shows Engineering rows with salary > 80000 (Frank, Charlie, Ivy)
Space fclear
These patterns directly map to Notion's column filter operators — no special syntax needed, just Python.
Conditional bulk replace workflow
Use this when you want to replace text only after narrowing rows by a condition:
gf
# Enter: name contains eza
gs
# Move to the target column, for example generated
g*
# Enter: ai<Tab>ai-bulk
This keeps the replacement limited to the filtered rows. g* works on selected rows, so gs matters.
Using this sample table:
| id | customer | status | priority | amount | |
|---|---|---|---|---|---|
| 1 | Alice | pending | high | 150.00 | alice@foo.com |
| 2 | Bob | shipped | low | 32.50 | bob@bar.com |
| 3 | Charlie | pending | low | 89.00 | charlie@gmail.com |
| 4 | Alice | completed | high | 200.00 | alice@foo.com |
| 5 | Diana | pending | medium | 512.00 | diana@gmail.com |
| 6 | Bob | cancelled | low | 15.00 | bob@bar.com |
| Notion operator | fx expression | Keeps rows | Explanation |
|---|---|---|---|
| is | status == "pending" | 1, 3, 5 | Exact match on column value |
| is-not | status != "shipped" | 1, 3, 4, 5, 6 | Not equal to value |
| contains | "gmail" in email | 3, 5 | Substring anywhere in column |
| not-contains | "gmail" not in email | 1, 2, 4, 6 | Substring absent from column |
| starts with | status.startswith("p") | 1, 3, 5 | Column value begins with text |
| ends with | email.endswith(".com") | 1, 2, 3, 4, 5, 6 | Column value ends with text |
Combining Conditions
Use and, or, and parentheses to build multi-column filters — all in a single fx expression. Using the same sample table:
fx expression | Keeps rows | Meaning |
|---|---|---|
status == "pending" and priority == "high" | 1 | pending AND high priority |
amount > 100 or priority == "high" | 1, 3, 4, 5 | amount > 100 OR high priority |
(status == "shipped" or status == "completed") and amount > 40 | 2 | shipped/completed AND amount > 40 |
"gmail" in email or customer == "Alice" | 1, 3, 4, 5 | gmail email OR customer is Alice |
# Quick reference — paste directly after `fx`:
amount > 500
status == 'done'
department != 'Engineering' and salary > 70000
"web" in hostname
region == 'sg' or region == 'kl'
Deduplication
# Step 1: Find duplicates on a key column
# Move to 'employee_id' column
Shift+F
# Step 2: Cast count column to int
#
# Step 3: Filter frequency table for duplicates
z|
# Enter: count > 1
# Step 4: Drill into source rows for a duplicate value
Enter
# Step 5: Select duplicate row to delete
s
# Step 6: Return to source and delete
q
gd
Top-N Filter
# Top 10 highest values (Excel Top 10 AutoFilter equivalent)
# Step 1: Cast to numeric type
# Move to 'amount' → %
# Step 2: Sort descending
]
# Step 3: Select top N rows
# Navigate to row N, then:
# Or use expression:
z|
# Enter: amount >= threshold
# Step 4: Open filtered sheet
"
Clear Selections
gu # unselect all rows
\ regex # unselect rows matching regex
z\ expr # unselect rows matching expression
Excel Filter Translation
| Excel / Google Sheets | VisiData | Description |
|---|---|---|
=FILTER(A:B, C:C="active") | | active → " | Filter by text match |
=FILTER(A:B, (C="X")*(D="Y")) | z| c == 'X' and d == 'Y' → " | Multi-condition |
=IF(A2>100, A2, "") | z| amount > 100 → " | Filter by condition |
| AutoFilter dropdown | | or , | Select by value |
| Advanced Filter (unique) | Shift+F | Unique value list |
| Remove duplicates | Shift+F → filter count>1 → gd | Deduplicate |
| Top 10 filter | ] sort → select → " | Sort descending first |
=UNIQUE(A:A) | Shift+F | Unique values |
| Slicers | | regex → " | Visual filter equivalent |
| Blank filter | z| value is None | Filter nulls |
| Number filter (> 100) | z| amount > 100 | Numeric condition |
=FILTER(A:B, ISNUMBER(SEARCH("keyword", A:A))) | Space fk → keyword | Keyword across all columns (OR) |
=FILTER(A:B, condition) | Space fx → expr | Python expression filter |
=FILTER(A:B, (A="X")*(B>N)) | Space ff → col is X and col > N | Natural-language cross-column filter |
=FILTER(A:B, (A="X")*(B>N)) | gf → col is X and col > N | Natural-language filter (keyboard shortcut) |
| AutoFilter on the active column | z Shift+C → is pending | Filter the cursor column without retyping its name |
| Clear filter | gc | Restore all rows (keyboard shortcut for fclear) |
Quick Reference
| Key | Action |
|---|---|
" | Open filtered sheet (selected rows only) |
g" | Open sheet copy (all rows, linked) |
gz" | Open deep copy (selected rows, independent) |
| regex | Select rows matching regex in current column |
g| regex | Select rows matching regex in any column |
z| expr | Select rows matching Python expression |
\ regex | Unselect rows matching regex |
z\ expr | Unselect rows matching expression |
, | Select rows matching current cell value |
g, | Select rows matching current row (all columns) |
gu | Unselect all rows |
gf | Filter rows by natural-language condition (shortcut for ff) |
z Shift+C | Filter the column under the cursor — type only <cond> <value> |
gc | Restore all rows (shortcut for fclear) |
Space → ff → Enter | Filter rows by natural-language condition (English operators, cross-column and/or) |
Space → fc → Enter | Filter the cursor column only (natural language, no column name) |
Space → fk → Enter | Filter rows by keyword (comma-separated, OR across columns) |
Space → fx → Enter | Filter rows by Python expression |
Space → fclear → Enter | Restore all rows (preserves added rows) |
Hands-On Practice
vd ~/github/practice-folder/visidata/01-loading/01-employees.csv
# 1. Filter by department: move to 'department' → | → Engineering → "
# 2. Save filtered: Ctrl+S → /tmp/engineering.csv
# 3. Press q → back to source
# 4. Filter by salary > 80000: z| → salary > 80000 → "
# 5. Press q → gu → clear all
# 6. Filter by value: move to 'Marketing' cell → , → "
What's Next
- Adding and Deleting Rows — add, delete, undo
- Copying and Pasting Rows — yank, paste, cut, cell copy