This is an automated email from the ASF dual-hosted git repository.
ColinLeeo pushed a commit to branch dataframe_desc
in repository https://gitbox.apache.org/repos/asf/tsfile.git
The following commit(s) were added to refs/heads/dataframe_desc by this push:
new e968332f8 feat(python): align dataframe description schema
e968332f8 is described below
commit e968332f8a0e6fd5656f72c0fb89ac4d57b288d8
Author: ColinLee <[email protected]>
AuthorDate: Sun Aug 9 13:31:26 2026 +0800
feat(python): align dataframe description schema
---
python/README-zh.md | 19 +++++---
python/README.md | 20 ++++++---
python/tests/test_tsfile_dataset.py | 90 +++++++++++++++++++++++++++++--------
python/tsfile/dataset/dataframe.py | 44 +++++++++++++-----
4 files changed, 133 insertions(+), 40 deletions(-)
diff --git a/python/README-zh.md b/python/README-zh.md
index 672ec1342..62b1f67fc 100644
--- a/python/README-zh.md
+++ b/python/README-zh.md
@@ -44,20 +44,26 @@
```json
{
- "weather": {
- "temperature": "C",
- "humidity": "T"
+ "multivariate": {
+ "weather": {
+ "columns": {
+ "temperature": "C",
+ "humidity": "T"
+ }
+ }
}
}
```
+未作为多变量训练数据使用的表可以不写入 `multivariate`。
+
创建 dataframe 时传入描述文件路径。`get` 和 `set` 提供对 JSON 顶层键的字典式
访问,其中 `set` 会将更新后的描述写回文件;另外还可以按表获取协变量列、目标
变量列及其数量:
```python
df = TsFileDataFrame("weather.tsfile", description_path="description.json")
-df.get("weather")
+df.get("multivariate")
df.get_covariate_columns("weather")
df.get_target_columns("weather")
df.get_covariate_column_count("weather")
@@ -69,7 +75,10 @@ df.get_device_statistics("weather")
df.get_device_point_count("weather", "device_a")
df.get_device_stats("weather", {"device": "device_a"})
df.list_device_metadata("weather")
-df.set("weather", {"temperature": "T", "humidity": "T"})
+df.set(
+ "multivariate",
+ {"weather": {"columns": {"temperature": "T", "humidity": "T"}}},
+)
df.close()
```
diff --git a/python/README.md b/python/README.md
index 424006e57..e068c09ad 100644
--- a/python/README.md
+++ b/python/README.md
@@ -41,20 +41,27 @@ covariate and `T` for a target column:
```json
{
- "weather": {
- "temperature": "C",
- "humidity": "T"
+ "multivariate": {
+ "weather": {
+ "columns": {
+ "temperature": "C",
+ "humidity": "T"
+ }
+ }
}
}
```
+Tables that are not used as multivariate training data may be omitted from
+`multivariate`.
+
Pass the description path when opening the dataframe. `get` and `set` provide
dictionary-style access to top-level JSON values; `set` writes the updated
description back to disk.
```python
df = TsFileDataFrame("weather.tsfile", description_path="description.json")
-df.get("weather")
+df.get("multivariate")
df.get_covariate_columns("weather")
df.get_target_columns("weather")
df.get_covariate_column_count("weather")
@@ -66,7 +73,10 @@ df.get_device_statistics("weather")
df.get_device_point_count("weather", "device_a")
df.get_device_stats("weather", {"device": "device_a"})
df.list_device_metadata("weather")
-df.set("weather", {"temperature": "T", "humidity": "T"})
+df.set(
+ "multivariate",
+ {"weather": {"columns": {"temperature": "T", "humidity": "T"}}},
+)
df.close()
```
diff --git a/python/tests/test_tsfile_dataset.py
b/python/tests/test_tsfile_dataset.py
index 3e42f9061..384751a68 100644
--- a/python/tests/test_tsfile_dataset.py
+++ b/python/tests/test_tsfile_dataset.py
@@ -266,9 +266,13 @@ def
test_dataset_description_get_set_and_column_roles(tmp_path):
description_path.write_text(
json.dumps(
{
- "weather": {
- "temperature": "C",
- "humidity": "T",
+ "multivariate": {
+ "weather": {
+ "columns": {
+ "temperature": "C",
+ "humidity": "T",
+ }
+ }
}
}
),
@@ -280,9 +284,13 @@ def
test_dataset_description_get_set_and_column_roles(tmp_path):
show_progress=False,
description_path=description_path,
) as tsdf:
- assert tsdf.get("weather") == {
- "temperature": "C",
- "humidity": "T",
+ assert tsdf.get("multivariate") == {
+ "weather": {
+ "columns": {
+ "temperature": "C",
+ "humidity": "T",
+ }
+ }
}
assert tsdf.get("missing") is None
assert tsdf.get("missing", {"default": True}) == {"default": True}
@@ -293,8 +301,8 @@ def
test_dataset_description_get_set_and_column_roles(tmp_path):
assert tsdf.get_covariate_columns("missing") == []
assert tsdf.get_target_column_count("missing") == 0
- description = tsdf.get("weather")
- description["temperature"] = "T"
+ description = tsdf.get("multivariate")
+ description["weather"]["columns"]["temperature"] = "T"
assert tsdf.get_covariate_columns("weather") == ["temperature"]
with pytest.raises(TypeError, match="must be JSON serializable"):
@@ -303,19 +311,27 @@ def
test_dataset_description_get_set_and_column_roles(tmp_path):
subset = tsdf[:1]
subset.set(
- "weather",
+ "multivariate",
{
- "temperature": "T",
- "humidity": "T",
+ "weather": {
+ "columns": {
+ "temperature": "T",
+ "humidity": "T",
+ }
+ }
},
)
assert tsdf.get_covariate_column_count("weather") == 0
assert tsdf.get_target_columns("weather") == ["temperature",
"humidity"]
assert json.loads(description_path.read_text(encoding="utf-8")) == {
- "weather": {
- "temperature": "T",
- "humidity": "T",
+ "multivariate": {
+ "weather": {
+ "columns": {
+ "temperature": "T",
+ "humidity": "T",
+ }
+ }
}
}
@@ -330,14 +346,46 @@ def
test_dataset_description_set_creates_missing_file(tmp_path):
show_progress=False,
description_path=description_path,
) as tsdf:
- assert tsdf.get("weather") is None
- tsdf.set("weather", {"temperature": "C", "humidity": "T"})
+ assert tsdf.get("multivariate") is None
+ tsdf.set(
+ "multivariate",
+ {
+ "weather": {
+ "columns": {
+ "temperature": "C",
+ "humidity": "T",
+ }
+ }
+ },
+ )
assert json.loads(description_path.read_text(encoding="utf-8")) == {
- "weather": {"temperature": "C", "humidity": "T"}
+ "multivariate": {
+ "weather": {
+ "columns": {
+ "temperature": "C",
+ "humidity": "T",
+ }
+ }
+ }
}
+def
test_dataset_description_allows_empty_or_unlisted_multivariate_tables(tmp_path):
+ path = tmp_path / "weather.tsfile"
+ description_path = tmp_path / "description.json"
+ _write_weather_file(path, 0)
+ description_path.write_text(json.dumps({"multivariate": {}}),
encoding="utf-8")
+
+ with TsFileDataFrame(
+ str(path),
+ show_progress=False,
+ description_path=description_path,
+ ) as tsdf:
+ assert tsdf.get_covariate_columns("weather") == []
+ assert tsdf.get_target_columns("weather") == []
+
+
def test_dataset_description_rejects_invalid_json_and_roles(tmp_path):
path = tmp_path / "weather.tsfile"
description_path = tmp_path / "description.json"
@@ -352,7 +400,8 @@ def
test_dataset_description_rejects_invalid_json_and_roles(tmp_path):
)
description_path.write_text(
- json.dumps({"weather": {"temperature": "X"}}), encoding="utf-8"
+ json.dumps({"multivariate": {"weather": {"columns": {"temperature":
"X"}}}}),
+ encoding="utf-8",
)
with TsFileDataFrame(
str(path),
@@ -362,7 +411,10 @@ def
test_dataset_description_rejects_invalid_json_and_roles(tmp_path):
with pytest.raises(ValueError, match="expected 'C' or 'T'"):
tsdf.get_covariate_columns("weather")
- tsdf.set("weather", {"temperature": ["C"]})
+ tsdf.set(
+ "multivariate",
+ {"weather": {"columns": {"temperature": ["C"]}}},
+ )
with pytest.raises(ValueError, match="expected 'C' or 'T'"):
tsdf.get_target_columns("weather")
diff --git a/python/tsfile/dataset/dataframe.py
b/python/tsfile/dataset/dataframe.py
index 0e61f1e15..52ea88b36 100644
--- a/python/tsfile/dataset/dataframe.py
+++ b/python/tsfile/dataset/dataframe.py
@@ -61,6 +61,8 @@ _OVERLAP_ROW_CHUNK_SIZE = 256
_COVARIATE_ROLE = "C"
_TARGET_ROLE = "T"
_DESCRIPTION_ROLES = {_COVARIATE_ROLE, _TARGET_ROLE}
+_MULTIVARIATE_KEY = "multivariate"
+_COLUMNS_KEY = "columns"
def _load_dataframe_description(description_path: Optional[str]) -> dict:
@@ -674,18 +676,24 @@ class TsFileDataFrame:
"""Lazy-loaded unified numeric dataset view over multiple TsFile shards.
``description_path`` optionally points to a JSON object describing the
- columns in each table. A description uses the following shape::
+ columns in multivariate tables. A description uses the following shape::
{
- "weather": {
- "temperature": "C",
- "humidity": "T"
+ "multivariate": {
+ "weather": {
+ "columns": {
+ "temperature": "C",
+ "humidity": "T"
+ }
+ }
}
}
- ``C`` marks a covariate column and ``T`` marks a target column. The
- description is independent of the TsFile data and is shared by dataframe
- subsets created through slicing or boolean selection.
+ Tables that are not used as multivariate training data do not need to be
+ listed under ``multivariate``. ``C`` marks a covariate column and ``T``
+ marks a target column. The description is independent of the TsFile data
+ and is shared by dataframe subsets created through slicing or boolean
+ selection.
"""
def __init__(
@@ -783,17 +791,31 @@ class TsFileDataFrame:
f"Description table name must be a string, got
{type(table_name)}"
)
- table_description = self._owner()._description.get(table_name)
+ multivariate_description =
self._owner()._description.get(_MULTIVARIATE_KEY)
+ if multivariate_description is None:
+ return []
+ if not isinstance(multivariate_description, dict):
+ raise ValueError("Description field 'multivariate' must be a JSON
object")
+
+ table_description = multivariate_description.get(table_name)
if table_description is None:
return []
if not isinstance(table_description, dict):
raise ValueError(
- f"Description for table '{table_name}' must be a JSON object"
+ f"Description for multivariate table '{table_name}' must be a
JSON object"
+ )
+
+ columns_description = table_description.get(_COLUMNS_KEY)
+ if columns_description is None:
+ return []
+ if not isinstance(columns_description, dict):
+ raise ValueError(
+ f"Description field 'multivariate.{table_name}.columns' must
be a JSON object"
)
invalid_roles = {
column: column_role
- for column, column_role in table_description.items()
+ for column, column_role in columns_description.items()
if not isinstance(column_role, str) or column_role not in
_DESCRIPTION_ROLES
}
if invalid_roles:
@@ -804,7 +826,7 @@ class TsFileDataFrame:
)
return [
column
- for column, column_role in table_description.items()
+ for column, column_role in columns_description.items()
if column_role == role
]