Dan Reverri
09/30/2025, 9:50 PMDesmond Cheong
09/30/2025, 10:02 PMDan Reverri
09/30/2025, 10:06 PMDan Reverri
09/30/2025, 10:07 PMDan Reverri
09/30/2025, 10:08 PMDan Reverri
09/30/2025, 10:08 PMCory Grinstead
09/30/2025, 10:10 PMcollect). Wondering what the code is doing. AFAIK, something like <http://df.to|df.to>_arrow_iter should not force a materialization.Dan Reverri
09/30/2025, 10:13 PMDan Reverri
10/01/2025, 1:36 AMimport argparse
import csv
import time
from typing import Literal
import daft
def string_data() -> str:
return "hello world, this is a test, with commas, and \"quotes\", and unicode: 測試"
def write_csv(name: str, data_type: Literal["string", "number"], num_rows: int = 1**6, num_cols: int = 100):
with open(name, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow([f"col_{i}" for i in range(num_cols)])
match data_type:
case "string":
data = string_data()
case "number":
data = 1.2345
writer.writerows([data for _ in range(num_cols)] for _ in range(num_rows))
def process_csv(name: str):
df = daft.read_csv(name)
batches = df.to_arrow_iter()
start = time.perf_counter()
next(batches)
end = time.perf_counter()
print(f"Read first batch in {end - start:.2f} seconds")
count = len(list(batches))
print(f"Read remaining {count} batches in {time.perf_counter() - end:.2f} seconds")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True)
write_parser = subparsers.add_parser("write", help="Write a large CSV file")
write_parser.add_argument("--rows", type=int, default=1**6, help="Number of rows to generate")
write_parser.add_argument("--cols", type=int, default=100, help="Number of columns to generate")
write_parser.add_argument("--type", type=str, choices=["string", "number"], default="string", help="Type of data to generate")
write_parser.add_argument("filename", type=str, help="The name of the file to write")
process_parser = subparsers.add_parser("process", help="Process a CSV file")
process_parser.add_argument("filename", type=str, help="The name of the file to process")
args = parser.parse_args()
if args.command == "write":
write_csv(args.filename, data_type=args.type, num_rows=args.rows, num_cols=args.cols)
elif args.command == "process":
process_csv(args.filename)
I used this to generate some sample data then process it:
DAFT_PROGRESS_BAR=false uv run daft_read_csv.py write --rows 1000000 --cols 100 --type number numbers.data.csv
DAFT_PROGRESS_BAR=false uv run daft_read_csv.py write --rows 1000000 --cols 10 --type string strings.data.csv
DAFT_PROGRESS_BAR=false uv run daft_read_csv.py process numbers.data.csv
Read first batch in 0.04 seconds
Read remaining 4679 batches in 1.44 seconds
DAFT_PROGRESS_BAR=false uv run daft_read_csv.py process strings.data.csv
Read first batch in 1.20 seconds
Read remaining 5780 batches in 0.24 seconds
The csv files were about the same size (669M for numbers, 764M for strings)Dan Reverri
10/01/2025, 1:37 AMDan Reverri
10/01/2025, 9:30 PMCory Grinstead
10/01/2025, 9:34 PMDesmond Cheong
10/02/2025, 12:01 AMthe first batch for string data takes a long time to yieldThe nice thing to note is that even though the first batch had a higher latency, end-to-end processing time for strings and numbers are roughly the same.
since your example has a lot of quotes and special charactersI do think this is relevant. In Daft we use a technique called speculative parsing to process a csv file in parallel. It's "speculative" because you can't simply cut a csv file into even chunks then parse it, you need to adjust the chunks to contain complete records, so we "speculate" where the boundaries are then adjust accordingly. This gives us lower end-to-end processing times because we can read a single CSV file in parallel on multiple threads. But for an "adversarial" case with many quotes, the first batch may take longer as we need to figure out what the proper chunk boundary is. The good news is that once we figure out the first batch, we can start downstream processing as we figure out subsequent batches, which is why end-to-end latency is still good
Dan Reverri
10/02/2025, 12:04 AMDesmond Cheong
10/02/2025, 12:07 AMDesmond Cheong
10/02/2025, 12:08 AMWith very large files this causes the process to use a lot of memory before yielding the first chunkthis shouldn't be an issue with speculative parsing either fwiw, since we process in 4MB chunks. The issue might lie elsewhere
Desmond Cheong
10/02/2025, 12:10 AMDesmond Cheong
10/02/2025, 2:43 AMDesmond Cheong
10/02/2025, 4:40 AMDesmond Cheong
10/07/2025, 5:02 PM