blob: ea926314925a4caee91fa5c7cb5991cc72dddcb6 [file]
# 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.
#
from typing import Iterator, Union
from typing import Optional
import numpy as np
import pandas as pd
from pandas.core.dtypes.common import is_integer_dtype, is_object_dtype
from pandas.core.interchange.dataframe_protocol import DataFrame
from tsfile import ColumnSchema, TableSchema, ColumnCategory, TSDataType, TIME_COLUMN
from tsfile.exceptions import TableNotExistError, ColumnNotExistError
from tsfile.tsfile_reader import TsFileReaderPy
from tsfile.tsfile_table_writer import (
TsFileTableWriter,
infer_object_column_type,
validate_dataframe_for_tsfile,
)
def to_dataframe(
file_path: str,
table_name: Optional[str] = None,
column_names: Optional[list[str]] = None,
start_time: Optional[int] = None,
end_time: Optional[int] = None,
max_row_num: Optional[int] = None,
as_iterator: bool = False,
) -> Union[pd.DataFrame, Iterator[pd.DataFrame]]:
"""
Read data from a TsFile and convert it into a Pandas DataFrame or
an iterator of DataFrames.
This function supports both table-model and tree-model TsFiles.
Users can filter data by table name, column names, time range,
and maximum number of rows.
Parameters
----------
file_path : str
Path to the TsFile to be read.
table_name : Optional[str], default None
Name of the table to query in table-model TsFiles.
If None and the file is in table model, the first table
found in the schema will be used.
column_names : Optional[list[str]], default None
List of column names to query.
- If None, all columns will be returned.
- Column existence will be validated in table-model TsFiles.
start_time : Optional[int], default None
Start timestamp for the query.
If None, the minimum int64 value is used.
end_time : Optional[int], default None
End timestamp for the query.
If None, the maximum int64 value is used.
max_row_num : Optional[int], default None
Maximum number of rows to read.
- If None, all available rows will be returned.
- When `as_iterator` is False, the final DataFrame will be
truncated to this size if necessary.
as_iterator : bool, default False
Whether to return an iterator of DataFrames instead of
a single concatenated DataFrame.
- True: returns an iterator yielding DataFrames in batches
- False: returns a single Pandas DataFrame
Returns
-------
Union[pandas.DataFrame, Iterator[pandas.DataFrame]]
- A Pandas DataFrame if `as_iterator` is False
- An iterator of Pandas DataFrames if `as_iterator` is True
Raises
------
TableNotExistError
If the specified table name does not exist in a table-model TsFile.
ColumnNotExistError
If any specified column does not exist in the table schema.
"""
def _gen(is_iterator: bool) -> Iterator[pd.DataFrame]:
_table_name = table_name
_column_names = column_names
_start_time = start_time if start_time is not None else np.iinfo(np.int64).min
_end_time = end_time if end_time is not None else np.iinfo(np.int64).max
## Time column handling (table model):
## 1. Request has no column list (query all):
## 1.1 TsFile has a time column in schema: query only non-time columns; then rename
## the first column of the returned DataFrame to the schema time column name.
## 1.2 TsFile has no time column in schema: query as-is; first column is "time".
## 2. Request has a column list but no time column:
## 2.1 TsFile has a time column in schema: query with requested columns; rename the
## first column to the schema time column name.
## 2.2 TsFile has no time column in schema: first column stays "time"; no rename.
## 3. Request has a column list including the time column:
## 3.1 Query with requested columns (including time); do not rename the first column.
with TsFileReaderPy(file_path) as reader:
total_rows = 0
table_schema = reader.get_all_table_schemas()
is_tree_model = len(table_schema) == 0
time_column = None
column_name_to_query = []
no_field_query = True
if is_tree_model:
if _column_names is None:
print("columns name is None, return all columns")
# When querying tables in the tree, only measurements are allowed currently.
no_field_query = False
else:
_table_name = _table_name.lower() if _table_name else None
_column_names = (
[column.lower() for column in _column_names]
if _column_names
else None
)
if _table_name is None:
_table_name, table_schema = next(iter(table_schema.items()))
else:
_table_name = _table_name.lower()
if _table_name.lower() not in table_schema:
raise TableNotExistError(_table_name)
table_schema = table_schema[_table_name]
column_names_in_file = []
for column in table_schema.get_columns():
if column.get_category() == ColumnCategory.TIME:
time_column = column.get_column_name()
else:
column_names_in_file.append(column.get_column_name())
if _column_names is not None:
for column in _column_names:
if column not in column_names_in_file and column != time_column:
raise ColumnNotExistError(column)
if (
table_schema.get_column(column).get_category()
== ColumnCategory.FIELD
):
no_field_query = False
if no_field_query:
if time_column is not None:
column_name_to_query.append(time_column)
column_name_to_query.extend(column_names_in_file)
else:
column_name_to_query = _column_names
else:
no_field_query = False
column_name_to_query = column_names_in_file
if is_tree_model:
if _column_names is not None:
column_name_to_query = _column_names
query_result = reader.query_table_on_tree(
column_name_to_query, _start_time, _end_time
)
else:
query_result = reader.query_table(
_table_name, column_name_to_query, _start_time, _end_time
)
with query_result as result:
while result.next():
if max_row_num is None:
dataframe = result.read_data_frame()
elif is_iterator:
dataframe = result.read_data_frame(max_row_num)
else:
remaining_rows = max_row_num - total_rows
if remaining_rows <= 0:
break
dataframe = result.read_data_frame(remaining_rows)
if dataframe is None or dataframe.empty:
continue
total_rows += len(dataframe)
if time_column is not None:
if _column_names is None or time_column not in _column_names:
dataframe = dataframe.rename(
columns={dataframe.columns[0]: time_column}
)
if no_field_query and _column_names is not None:
_column_names.insert(0, TIME_COLUMN)
dataframe = dataframe[_column_names]
yield dataframe
if (
(not is_iterator)
and max_row_num is not None
and total_rows >= max_row_num
):
break
if as_iterator:
return _gen(True)
else:
df_list = list(_gen(False))
if df_list:
df = pd.concat(df_list, ignore_index=True)
if max_row_num is not None and len(df) > max_row_num:
df = df.iloc[:max_row_num]
return df
else:
return pd.DataFrame()
def dataframe_to_tsfile(
dataframe: pd.DataFrame,
file_path: str,
table_name: Optional[str] = None,
time_column: Optional[str] = None,
tag_column: Optional[list[str]] = None,
):
"""
Write a pandas DataFrame to a TsFile by inferring the table schema from the DataFrame.
This function automatically infers the table schema based on the DataFrame's column
names and data types, then writes the data to a TsFile.
Parameters
----------
dataframe : pd.DataFrame
The pandas DataFrame to write to TsFile.
- If a 'time' column (case-insensitive) exists, it will be used as the time column.
- Otherwise, the DataFrame index will be used as timestamps.
- All other columns will be treated as data columns.
file_path : str
Path to the TsFile to write. Will be created if it doesn't exist.
table_name : Optional[str], default None
Name of the table. If None, defaults to "default_table".
time_column : Optional[str], default None
Name of the time column. If None, will look for a column named 'time' (case-insensitive),
or use the DataFrame index if no 'time' column is found.
tag_column : Optional[list[str]], default None
List of column names to be treated as TAG columns. All other columns will be FIELD columns.
If None, all columns are treated as FIELD columns.
Returns
-------
None
Raises
------
ValueError
If the DataFrame is empty or has no data columns.
"""
validate_dataframe_for_tsfile(dataframe)
df = dataframe.rename(columns=str.lower)
if not table_name:
table_name = "default_table"
if time_column is not None:
if time_column.lower() not in df.columns:
raise ValueError(f"Time column '{time_column}' not found in DataFrame")
if tag_column is not None:
for tag_col in tag_column:
if tag_col.lower() not in df.columns:
raise ValueError(f"Tag column '{tag_col}' not found in DataFrame")
tag_columns_lower = {t.lower() for t in (tag_column or [])}
if time_column is not None:
time_col_name = time_column.lower()
elif "time" in df.columns:
time_col_name = "time"
else:
time_col_name = None
if time_col_name is not None:
if not is_integer_dtype(df[time_col_name].dtype):
raise TypeError(
f"Time column '{time_col_name}' must be integer type (int64 or int), got {df[time_col_name].dtype}"
)
column_schemas = []
if time_col_name is not None:
column_schemas.append(
ColumnSchema(time_col_name, TSDataType.TIMESTAMP, ColumnCategory.TIME)
)
for col in df.columns:
if col == time_col_name:
continue
col_dtype = df[col].dtype
if is_object_dtype(col_dtype):
ts_data_type = infer_object_column_type(df[col])
else:
ts_data_type = TSDataType.from_pandas_datatype(col_dtype)
category = (
ColumnCategory.TAG if col in tag_columns_lower else ColumnCategory.FIELD
)
column_schemas.append(ColumnSchema(col, ts_data_type, category))
data_columns = [
s for s in column_schemas if s.get_category() != ColumnCategory.TIME
]
if len(data_columns) == 0:
raise ValueError(
"DataFrame must have at least one data column besides the time column"
)
table_schema = TableSchema(table_name, column_schemas)
with TsFileTableWriter(file_path, table_schema) as writer:
writer.write_dataframe(df)