tamnd / tamnd/firepanda

L. The four column types three conformance sections are waiting on

Open
#270 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Mojo
Stars
1
Forks
0
Avg merge
1h 31m
Merged PRs (30d)
640

Description

Found by the pandas conformance suite, and it is the largest single cause it has ever reported. Specification: [`13-the-type-gap.md`](https://github.com/tamnd/firepanda-compat/blob/main/docs/specs/13-the-type-gap.md).

Of the 334 runs failing against firepanda today, 228 never ran an operation. The frame could not be read. Three whole sections of the suite score exactly zero, and they score zero for one reason rather than for many.

### The measurement

Per section, against pandas 3.0.3 and firepanda at `46ddd6a`.

| section | pass | fail | divergent | unimplemented |
| --- | --- | --- | --- | --- |
| basics | 232 | 92 | 1 | 168 |
| categorical | 0 | 61 | 0 | 3 |
| groupby | 111 | 5 | 0 | 57 |
| nested | 0 | 36 | 0 | 0 |
| stats | 52 | 4 | 0 | 51 |
| temporal | 0 | 121 | 0 | 1 |

All 121 temporal failures, all 61 categorical failures and 35 of the 36 nested failures are the same sentence in different words, and the sentence is that `read_arrow` refused the file.

| what the reader refused | frames | runs |
| --- | --- | --- |
| Arrow type 10, a timestamp | `temporal_range`, `temporal_resolutions`, `temporal_dst_back`, `temporal_dst_forward`, `temporal_dst_lord_howe` | 118 |
| a dictionary encoded column | `categorical_ordered`, `categorical_unordered` | 68 |
| a nested type | `nested_list`, `nested_struct`, `nested_deep` | 36 |
| Arrow type 18, a duration | `temporal_durations` | 6 |

217 of those are inside the three zero sections and 11 are cases in `groupby`, `basics`, `reshape`, `stats`, `indexing` and `windows` that happen to name one of those frames.

So two thirds of everything the suite calls a failure is not a wrong answer. It is a file that could not be opened.

### What it is

`firepanda/dtype/logical.mojo` has a `TypeKind` with six members: `NULL`, `BOOL`, `INT`, `FLOAT_KIND`, `STRING`, `BINARY`. `LogicalType` builds fifteen types out of them and that is the whole type system. There is no timestamp, no duration, no dictionary and no nested type anywhere in the library, not in the reader, not in the kernels and not on the Python side.

`_format_for` in `firepanda/io/arrow_ipc.mojo` is where a user meets that. It maps a schema field to a C Data Interface format string and raises when there is no format string to map to, which is every temporal type, every decimal, every interval, anything dictionary encoded and anything with children.

The refusals are correct and the messages are good. `column 'second' has Arrow type 10, which firepanda cannot read yet` names the column and the type and says the honest thing. There is no bug in the reader. There is a type system with a hole in it.

### Why this was not already scoped

Two of the four types are already M6 issues and both are written one level too high. #159 is categoricals, "8 callables plus the dtype and the `observed` paths". #160 is nested data, "`.list`, `.struct`, `explode`, `json_normalize`". Neither says that the first thing standing in front of those methods is a column type that does not exist and a reader that refuses the file. That is not a mistake in the issues, it is what a scope written before there was a measurement looks like.

The temporal case is a real hole. There is no temporal workstream in M6 at all. The fourteen are the index, strings, categoricals, nested, windows, reshaping, groupby, statistics, the cheap surface, errors and `api.types`, and a datetime column is in none of them. #9 is M7, Time series, and its scope is `resample`, `merge_asof`, time based rolling, timezones, `date_range`, the forty offsets, `at_time`, `between_time`, Period and Interval. Every item on that list assumes a timestamp column already exists to operate on.

So a datetime column fell between two milestones. M6 did not claim it because M6 is organized by pandas namespace and a column type is not a namespace. M7 did not claim it because M7 is the algorithms and it took the column for granted.

### The size of it

From `surface/pandas-3.0.3.json`, where pandas 3.0.3 is 1125 public callables and 1413 names.

| namespace | names | callables |
| --- | --- | --- |
| `dt` | 42 | 13 |
| `Timestamp` | 75 | 42 |
| `Timedelta` | 22 | 10 |
| `DatetimeIndex` | 144 | 91 |
| `offsets` | 47 | 46 |
| `Resampler` | 34 | 27 |
| `cat` | 11 | 8 |
| `list` | 2 | 2 |
| `struct` | 3 | 2 |

The temporal six are 364 names and 229 callables, 20 percent of the pandas callable surface, sitting behind a column type nothing can construct.

`dt` is the one to read carefully. 42 names and only 13 callables, because 29 of them are properties: `dt.year`, `dt.month`, `dt.hour`, `dt.is_leap_year`, `dt.days_in_month` and the rest. A property is not a callable and does not appear in the callable denominator, so an L3 rate over `dt` measures 13 names while the work is on 42, and the suite's `temporal` section already has cases for all 42.

### Scope

Four column types, in this order, ordered by what unblocks the most per unit of work rather than by what is most interesting.

- [x] **The timestamp.** A `TIMESTAMP` kind with a unit and an optional time zone, stored as int64, which is what Arrow stores and what pandas stores. The reader maps Arrow type 10 to it honouring the unit rather than normalizing to nanoseconds, because `temporal_resolutions` exists in the corpus specifically to hold a second, a millisecond, a microsecond and a nanosecond column side by side, and a reader that normalized would pass that frame by destroying the thing it tests. Unblocks 118 runs. Done in #272, which also had to add date32, because `temporal_range` carries a date column behind its timestamp column and the reader stops at the first refusal. Measured movement is 125 runs, not 118.
- [x] **The duration.** A `DURATION` kind, also int64 with a unit, mapped from Arrow type 18. Six runs, and it is here rather than later because subtracting two timestamps produces one and `Series.sub` on a timestamp column is already a case in the suite. Done in #275. Six runs, exactly as predicted, and they move to `unimplemented` rather than to a pass.
- [x] **The dictionary.** A values array, an index array and an ordered flag. 68 runs and the whole of #159. Third rather than first because the categorical section is smaller than the temporal one, and because a dictionary column touches every kernel that dispatches on type where a timestamp is an int64 wearing a hat. Done in #278. 74 runs rather than 68, and the categorical section now has no failures in it at all. The C Data Interface half is left refused and is #277.
- [x] **The nested types.** List and struct, mapped from a field with children. 36 runs and the whole of #160. Done in #282. 37 runs, not 36, because a reshape case names a nested frame too. The tree is held flat, a node per child naming its parent, since Mojo will not let a struct hold a `List` of itself, and that turned out to be the shape Arrow already describes a batch in. Writing one back out is #284 and joining one across record batches is #285, both refused by name.
- [ ] **The `dt` accessor**, scoped in #287 and specified in [`14-the-dt-accessor.md`](https://github.com/tamnd/firepanda-compat/blob/main/docs/specs/14-the-dt-accessor.md), all 42 names, plus `pandas.to_datetime`, the plain form of `pandas.date_range`, and `Series.dtype` reporting a datetime dtype. This is what the `temporal` section actually asks for and none of it needs an M7 algorithm.

### What stays out

`DatetimeIndex` and its 91 callables go with the index work in #154 and #155, because it is an index before it is a datetime.

`offsets` and `Resampler` stay in M7 entirely, along with `resample`, `merge_asof`, time based rolling and the DST arithmetic. This issue is the column. M7 is what you do with it.

Time zone handling here is the column carrying a zone name and the reader preserving it, not the DST correctness suite that #9 asks for. `dt.tz_localize` and `dt.tz_convert` are 16 of the 121 temporal runs and they are in scope, and the ambiguous and nonexistent local time policy is M7's.

### Exit criteria

- [x] The `temporal`, `categorical` and `nested` sections are no longer zero, and no run in any of them fails at the read
- [x] The four corpus frame families read without an error, at every unit and every zone the corpus holds
- [ ] A round trip: a frame read from Arrow IPC and written back is byte identical in its schema, including the timestamp unit, the zone name and the dictionary ordered flag
- [x] The failure count on the board falls by at least 228, measured rather than predicted, with the run posted. 334 to 94, a fall of 240, posted below

### Depends on

Nothing. It is the front of the queue.

### Blocks

#159, #160, and every temporal item in #9.

Contributor guide

Open the contributing guide

Research direction

Start with firepanda/dtype/logical.mojo and firepanda/io/arrow_ipc.mojo, then read issue #287 and docs/specs/14-the-dt-accessor.md for the remaining dt scope. Run the temporal conformance section first. Done means the listed datetime accessor names and related datetime operations work without read failures, while preserving the stated exclusions for indexes and M7 algorithms.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data-engineering
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.