| <!-- | |
| Licensed to the Apache Software Foundation (ASF) under one | |
| or more contributor license agreements. See the NOTICE file | |
| distributed with this work for additional information | |
| regarding copyright ownership. The ASF licenses this file | |
| to you under the Apache License, Version 2.0 (the | |
| "License"); you may not use this file except in compliance | |
| with the License. You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, | |
| software distributed under the License is distributed on an | |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| KIND, either express or implied. See the License for the | |
| specific language governing permissions and limitations | |
| under the License. | |
| --> | |
| # TsFileDataFrame | |
| `TsFileDataFrame` lets you read the numeric measurements inside one or more TsFiles the | |
| same way you would work with a pandas DataFrame — without having to care about | |
| the underlying file format or data-loading details. It is part of the TsFile Python | |
| package (`pip install tsfile`). | |
| ## Quick start | |
| ```python | |
| from tsfile import TsFileDataFrame | |
| df = TsFileDataFrame("table_data/") # load every .tsfile under the directory | |
| print(df) # browse all series (metadata only) | |
| ts = df["weather.Beijing.humidity"] # pick one series (lazy handle) | |
| window = ts[20:100] # slice by row index -> np.ndarray | |
| data = df.loc[start:end, [ # align multiple series on timestamps | |
| "weather.Beijing.temperature", | |
| "weather.Beijing.humidity", | |
| ]] | |
| data.values # -> np.ndarray, shape (N, 2): N timestamps × 2 series | |
| ``` | |
| ## Core types | |
| `TsFileDataFrame` is built around three types: | |
| - **`TsFileDataFrame`** — the entry point. It loads one or more TsFiles and | |
| exposes a unified view. Construction only scans metadata; **no values are read**. | |
| - **`Timeseries`** — a lazy handle to a single series, obtained from `df[...]`. | |
| It carries the series' metadata but reads nothing until you index it by row. | |
| - **`AlignedTimeseries`** — the result of aligning several series on a common | |
| time axis, obtained from `df.loc[...]`. It reads the requested range into | |
| memory at once: the aligned timestamp array (`.timestamps`, length **N**) and a | |
| value matrix (`.values`, shape **(N, M)**) — **N** timestamps (rows) × **M** | |
| selected series (columns). | |
| ### TsFileDataFrame | |
| In the table below, `df` is a `TsFileDataFrame` instance, created with | |
| `df = TsFileDataFrame(paths)`. | |
| | Example | Operation | Returns | | |
| |---|---|---| | |
| | `TsFileDataFrame(paths)` | Load a file / list of files / directory | `TsFileDataFrame` | | |
| | `len(df)` | Number of time series | `int` | | |
| | `df.list_timeseries("weather")` | Series names, optionally filtered by prefix | `List[str]` | | |
| | `df["weather.Beijing.humidity"]`, `df[0]`, `df[-1]` | One series | `Timeseries` | | |
| | `df["city"]` | A metadata column (a tag / `field` / `start_time` / `end_time` / `count`) | `pandas.Series` | | |
| | `df[0:3]`, `df[[0, 2, 5]]` | Subset view by integer position: a contiguous range (`0:3`), or the listed positions (`[0, 2, 5]`); positions are the printed `index` column | `TsFileDataFrame` | | |
| | `df[df["city"] == "Beijing"]` | Filter by a metadata column | `TsFileDataFrame` | | |
| | `df.loc[start:end, series_list]` | Timestamp-aligned query | `AlignedTimeseries` | | |
| | `df.show(max_rows=20)` / `print(df)` | Print the metadata table | — | | |
| | `df.close()` | Release file handles | — | | |
| ### Timeseries | |
| In the table below, `ts` is a `Timeseries`, obtained from `ts = df[...]`. | |
| | Example | Operation | Returns | | |
| |---|---|---| | |
| | `ts.name` | Series name | `str` | | |
| | `len(ts)` | Number of points | `int` | | |
| | `ts.stats` | Series statistics | `dict` (`start_time`, `end_time`, `count`) | | |
| | `ts[20]` | Single value | `float` (or `None` if null) | | |
| | `ts[20:100]` | Row-range slice | `np.ndarray` | | |
| | `ts.timestamps` | Timestamp array | `np.ndarray` | | |
| ### AlignedTimeseries | |
| In the table below, `data` is an `AlignedTimeseries`, obtained from | |
| `data = df.loc[...]`. | |
| | Example | Operation | Returns | | |
| |---|---|---| | |
| | `data.shape` | Shape `(N, M)` — N timestamps, M series | `tuple` | | |
| | `data.timestamps` | Timestamp array | `np.ndarray` | | |
| | `data.values` | Value matrix | `np.ndarray`, shape `(N, M)` | | |
| | `data.series_names` | Series names | `List[str]` | | |
| | `len(data)` | Number of rows | `int` | | |
| | `data[0]`, `data[0:10]`, `data[0, 1]` | Row / element indexing | `np.ndarray` / scalar | | |
| | `data.show(50)` / `print(data)` | Formatted output (auto-truncated) | — | | |
| ## Series names | |
| A series is uniquely identified by its **series name**, a string formed by | |
| joining the **table name**, the **tag-column values**, and the **field name** | |
| with `.`, in that order: | |
| ```text | |
| {table_name}.{tag_value_1}.{tag_value_2}...{field_name} | |
| ``` | |
| `list_timeseries()` returns `SeriesPath` objects — a `str` subclass whose value | |
| is the escaped path above, so each works directly as a key in `df[...]` and | |
| `df.loc[...]`. A `SeriesPath` also exposes its parts as `.table`, `.tags` (a | |
| tuple, with `None` for a null tag), and `.field`. | |
| Examples: | |
| - `weather.Beijing.humidity` — table `weather`, tag `Beijing`, field `humidity` | |
| - `sensor.s1.pressure` — table `sensor`, tag `s1`, field `pressure` | |
| **Escaping in a name.** `.` separates the parts, so a `.` inside a table, tag, or | |
| field name is escaped with a backslash, and a null tag value is written as `\N` | |
| (distinct from the literal string `"null"`). `list_timeseries()` returns this | |
| escaped form — e.g. tag value `Bei.jing` in table `weather` renders as | |
| `weather.Bei\.jing.humidity` (a literal `\` becomes `\\`). Selecting needs the | |
| same escaped form: reuse the `SeriesPath` from `list_timeseries()` (or read its | |
| `.table` / `.tags` / `.field` parts), or write one as a raw string to keep the | |
| backslash: | |
| ```python | |
| df[r"weather.Bei\.jing.humidity"] # tag "Bei.jing" (the dot is part of the value) | |
| df[r"weather.\N.Beijing.humidity"] # tags (null, "Beijing") | |
| ``` | |
| > A series name can be obtained from `list_timeseries()` and need not be | |
| > constructed by hand; a series may also be selected by integer index (`df[0]`) | |
| > or metadata filter (`df[df["city"] == "Beijing"]`). | |
| ## Loading | |
| A path may be a single file, a directory, or a list mixing files and directories: | |
| ```python | |
| from tsfile import TsFileDataFrame | |
| df = TsFileDataFrame(["data/weather.tsfile", "data/sensor.tsfile"]) | |
| df = TsFileDataFrame("data/") # recursively find every .tsfile under the directory | |
| print(df) | |
| ``` | |
| Construction only scans metadata; actual values are not read. When several files | |
| are loaded, their metadata is scanned in parallel, using up to | |
| `min(number_of_files, CPU cores)` threads; a single file is scanned serially. | |
| Only numeric **field** columns hold readable data (`BOOLEAN`, `INT32`, `INT64`, | |
| `FLOAT`, `DOUBLE`, `TIMESTAMP`); non-numeric fields (`STRING`, `TEXT`, `BLOB`, | |
| `DATE`) are skipped during loading and never become series. **Tag** columns are | |
| unaffected — string tags are fully supported as device identifiers and metadata | |
| (series names, the `df["city"]` column, metadata filters). | |
| If several files contain the **same series** (e.g. daily shards of | |
| `weather.Beijing.humidity`), they are merged into one continuous series. Their | |
| timestamps must not conflict across shards; a duplicate timestamp raises an error | |
| when the series is read. Deduplicate during preprocessing. | |
| ### Displaying a DataFrame | |
| `print(df)` (and `df.show(max_rows=...)`) prints series metadata, head/tail | |
| truncated when large. The header is: | |
| ```text | |
| index │ table │ <tag1> │ <tag2> │ ... │ field │ start_time │ end_time │ count | |
| ``` | |
| The tag columns shown are the union of every table's tag-column names (in | |
| first-seen order). Each row fills only the tag columns its own table defines; | |
| other tag columns are left blank, and a null tag value shows as `None`. | |
| ```text | |
| TsFileDataFrame(table model, 972 time series, 5 files) | |
| table ps_id sn frac field start_time end_time count | |
| 0 pvf 10 30100194A00234H00572 1 pac 2024-04-02 00:00:00 2024-10-28 23:45:00 20160 | |
| 1 pvf 10 30100194A00234H00572 1 tenmeterswindspeed 2024-04-02 00:00:00 2024-10-28 23:45:00 20160 | |
| ... | |
| ``` | |
| ## Browsing series | |
| `list_timeseries(path_prefix="")` lists the series names in the loaded files, | |
| optionally filtered by a prefix. Calling it with no argument returns all series. | |
| ```python | |
| >>> df.list_timeseries("weather") | |
| ['weather.Beijing.humidity', 'weather.Beijing.temperature', | |
| 'weather.Shanghai.humidity', 'weather.Shanghai.temperature'] | |
| >>> df.list_timeseries("weather.Beijing") | |
| ['weather.Beijing.humidity', 'weather.Beijing.temperature'] | |
| ``` | |
| To inspect metadata such as start/end time and count, print the DataFrame (or a | |
| subset of it) — see [Displaying a DataFrame](#displaying-a-dataframe). | |
| ## Selecting series | |
| `df[...]` returns a lazy `Timeseries` handle (no data read) or a subset view: | |
| ```python | |
| ts = df["weather.Beijing.humidity"] # by name | |
| ts = df[0] # by index (negative indices allowed) | |
| sub_df = df[0:3] # slice -> TsFileDataFrame (view) | |
| sub_df = df[[0, 2, 5]] # integer list -> TsFileDataFrame (view) | |
| sub_df = df[df["city"] == "Beijing"] # metadata filter -> TsFileDataFrame (view) | |
| ``` | |
| ```text | |
| >>> df["weather.Beijing.humidity"] | |
| Timeseries('weather.Beijing.humidity', count=2880, start=2026-01-27 00:00:00, end=2026-02-05 23:55:00) | |
| ``` | |
| Series metadata is served from cache (no I/O): | |
| ```python | |
| >>> ts = df["weather.Beijing.humidity"] | |
| >>> ts.name | |
| 'weather.Beijing.humidity' | |
| >>> len(ts) | |
| 2880 | |
| >>> ts.stats | |
| {'start_time': 1769443200000, 'end_time': 1770306900000, 'count': 2880} | |
| ``` | |
| ## Reading data | |
| Indexing a `Timeseries` by row triggers the actual file read: | |
| ```python | |
| val = ts[20] # -> float | |
| window = ts[20:100] # -> np.ndarray, shape = (80,) | |
| last_ten = ts[-10:] # -> np.ndarray | |
| sampled = ts[::2] # -> np.ndarray (strided sampling) | |
| ts.timestamps[20:100] # -> the timestamps for those rows, np.ndarray | |
| ``` | |
| ```text | |
| >>> ts[20] | |
| 46.1 | |
| >>> ts[20:100] | |
| array([46.1 , 41.72, 52.94, ..., 76.3 , 84.35]) | |
| >>> ts.timestamps[20:100] | |
| array([1769449200000, 1769449500000, ..., 1769472900000]) | |
| ``` | |
| ## Timestamp-aligned queries | |
| When you need several series strictly aligned on one time axis, use `.loc`: | |
| ```python | |
| data = df.loc[start_time:end_time, [ | |
| "weather.Beijing.humidity", | |
| "weather.Beijing.temperature", | |
| "sensor.s1.pressure", | |
| ]] | |
| ``` | |
| The returned `AlignedTimeseries` aligns all series to the **union** of their | |
| timestamps and fills missing positions with `NaN`: | |
| ```python | |
| data.timestamps # np.ndarray, millisecond timestamps | |
| data.values # np.ndarray, shape = (N, 3) | |
| data.series_names # ["weather.Beijing.humidity", ...] | |
| data.shape # (N, 3) | |
| data[0:10] # first 10 rows, np.ndarray shape = (10, 3) | |
| data.show(50) # show up to 50 rows | |
| ``` | |
| Series may be given by name or by index, mixed freely: | |
| ```python | |
| df.loc[start_time:end_time, [0, 1, 4]] | |
| df.loc[start_time:end_time, [0, "weather.Beijing.temperature", 4]] | |
| ``` | |
| ```text | |
| >>> df.loc[1769616000000:1769702100000, | |
| ... ['weather.Beijing.temperature', 'weather.Beijing.humidity', 'sensor.s2.pressure']] | |
| AlignedTimeseries(288 rows, 3 series) | |
| timestamp weather.Beijing.temperature weather.Beijing.humidity sensor.s2.pressure | |
| 2026-01-29 00:00:00 29.12 92.87 NaN | |
| 2026-01-29 00:05:00 1.55 87.34 NaN | |
| ... | |
| ``` | |
| Printing the result shows the time column to the left of the values, but the | |
| `.values` matrix holds only the value columns — read the aligned timestamps from | |
| `df.loc[...].timestamps`. | |
| ## Closing | |
| A `with` block closes file handles automatically; you can also close manually: | |
| ```python | |
| with TsFileDataFrame("data/") as df: | |
| ... # handles released on exit | |
| tsdf = TsFileDataFrame("data/") | |
| tsdf.close() # or close it yourself | |
| ``` |