Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[build-system]

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pyproject.toml pins setuptools<70 as a build requirement. A hard upper bound can unnecessarily block builds on environments that have already moved to newer setuptools. If this cap is required due to a known incompatibility (e.g., PyScaffold <4), please add a brief rationale/comment or link; otherwise consider relaxing/removing the upper bound.

Suggested change
[build-system]
[build-system]
# Keep setuptools below 70 while using PyScaffold < 4, which has known
# compatibility constraints with newer setuptools releases.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think mentioning that this is a requirement from pyscaffold is indeed a good idea

# setuptools is capped below 70 because PyScaffold <4 (used by this project)
# has known compatibility issues with setuptools>=70. Once the project
# migrates to PyScaffold 4+, this upper bound can be removed.
requires = ["setuptools<70", "wheel", "pyscaffold>=3.3a0,<4"]
build-backend = "setuptools.build_meta"
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ install_requires =
pywavelets
scikit-learn>=0.22.0
tqdm>=4.10.0
stumpy>=1.7.2
stumpy>=1.11.1
cloudpickle

# Require a specific Python version, e.g. Python 2.7 or >= 3.4
Expand Down
10 changes: 0 additions & 10 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@
PyScaffold helps you to put up the scaffold of your new Python project.
Learn more under: https://pyscaffold.org/
"""
import sys

from pkg_resources import VersionConflict, require
from setuptools import setup

try:
require("setuptools>=38.3")
except VersionConflict:
print("Error: version of setuptools is too old (<38.3)!")
sys.exit(1)


if __name__ == "__main__":
setup(use_pyscaffold=True)
3 changes: 2 additions & 1 deletion tests/integrations/examples/test_robot_execution_failures.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from unittest import TestCase

import numpy as np
import pandas as pd
from pandas import DataFrame, Series

from tsfresh import extract_features
Expand Down Expand Up @@ -54,4 +55,4 @@ def test_multilabel_target_on_request(self):
)

assert len(y.unique()) > 2
assert y.dtype == object
assert y.dtype == object or isinstance(y.dtype, pd.StringDtype)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you probably want to import pd here :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nils-braun Thanks for the approval! I will gladly take care of these last few items. I’ll fix the missing pd import, clear up the linting, and dig into those two 3.11/3.12 test failures today. I'll push an update shortly!

10 changes: 5 additions & 5 deletions tests/units/feature_selection/test_relevance.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ def test_constant_feature_irrelevant(self, y_binary):

relevance_table = calculate_relevance_table(X, y_binary)
assert "feature_binary" == relevance_table.index[0]
assert "constant" == relevance_table.type[0]
assert np.isnan(relevance_table.p_value[0])
assert not relevance_table.relevant[0]
assert "constant" == relevance_table.type.iloc[0]
assert np.isnan(relevance_table.p_value.iloc[0])
assert not relevance_table.relevant.iloc[0]

@mock.patch("tsfresh.feature_selection.relevance.target_binary_feature_real_test")
@mock.patch("tsfresh.feature_selection.relevance.target_binary_feature_binary_test")
Expand Down Expand Up @@ -166,9 +166,9 @@ def test_multiclass_correct_features_relevant(self, y_multi):
X_multi = pd.DataFrame()
X_multi["relevant_0"] = np.concatenate([np.zeros(298), np.array([0.01, -0.01])])
X_multi["relevant_3"] = X_multi["relevant_0"].copy()
X_multi["relevant_3"][y_multi == 0] = np.random.uniform(2, 3, 100)
X_multi.loc[y_multi == 0, "relevant_3"] = np.random.uniform(2, 3, 100)
X_multi["relevant_2"] = X_multi["relevant_3"].copy()
X_multi["relevant_2"][y_multi == 1] = np.random.uniform(-2, -1, 100)
X_multi.loc[y_multi == 1, "relevant_2"] = np.random.uniform(-2, -1, 100)

relevance_table = calculate_relevance_table(
X_multi, y_multi, multiclass=True, ml_task="classification", n_significant=3
Expand Down
2 changes: 1 addition & 1 deletion tests/units/transformers/test_feature_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def test_multiclass_relevant_features_selected(self):
X_multi = pd.DataFrame()
X_multi["irrelevant"] = np.concatenate([np.zeros(298), np.array([0.01, -0.01])])
X_multi["relevant"] = X_multi["irrelevant"].copy()
X_multi["relevant"][y_multi == 0] = np.random.uniform(2, 3, 100)
X_multi.loc[y_multi == 0, "relevant"] = np.random.uniform(2, 3, 100)

selector = FeatureSelector(
multiclass=True, n_significant=3, ml_task="classification"
Expand Down
27 changes: 18 additions & 9 deletions tests/units/transformers/test_per_column_imputer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ def test_only_nans_and_infs(self):
with warnings.catch_warnings(record=True) as w:
imputer.fit(X)
self.assertEqual(len(w), 1)
self.assertEqual(
"The columns ['NaNs' 'PINF' 'NINF'] did not have any finite values. Filling with zeros.",
str(w[0].message),
warning_msg = str(w[0].message)
self.assertIn("'NaNs'", warning_msg)
self.assertIn("'PINF'", warning_msg)
self.assertIn("'NINF'", warning_msg)
self.assertIn(
"did not have any finite values. Filling with zeros.", warning_msg
)

selected_X = imputer.transform(X)
Expand All @@ -61,9 +64,12 @@ def test_with_numpy_array(self):
with warnings.catch_warnings(record=True) as w:
imputer.fit(X)
self.assertEqual(len(w), 1)
self.assertEqual(
"The columns ['NaNs' 'PINF' 'NINF'] did not have any finite values. Filling with zeros.",
str(w[0].message),
warning_msg = str(w[0].message)
self.assertIn("'NaNs'", warning_msg)
self.assertIn("'PINF'", warning_msg)
self.assertIn("'NINF'", warning_msg)
self.assertIn(
"did not have any finite values. Filling with zeros.", warning_msg
)

selected_X = imputer.transform(X)
Expand All @@ -73,9 +79,12 @@ def test_with_numpy_array(self):
with warnings.catch_warnings(record=True) as w:
imputer.fit(X_numpy)
self.assertEqual(len(w), 1)
self.assertEqual(
"The columns [0 1 2] did not have any finite values. Filling with zeros.",
str(w[0].message),
warning_msg = str(w[0].message)
self.assertIn("0", warning_msg)
self.assertIn("1", warning_msg)
self.assertIn("2", warning_msg)
self.assertIn(
"did not have any finite values. Filling with zeros.", warning_msg
)

selected_X_numpy = imputer.transform(X_numpy)
Expand Down
13 changes: 7 additions & 6 deletions tests/units/utilities/test_dataframe_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,9 +1351,10 @@ def test_no_finite_values_yields_0(self):
) = dataframe_functions.get_range_values_per_column(df)

self.assertEqual(len(w), 1)
self.assertEqual(
str(w[0].message),
"The columns ['value'] did not have any finite values. Filling with zeros.",
warning_msg = str(w[0].message)
self.assertIn("'value'", warning_msg)
self.assertIn(
"did not have any finite values. Filling with zeros.", warning_msg
)

self.assertEqual(col_to_max, {"value": 0})
Expand Down Expand Up @@ -1405,7 +1406,7 @@ def test_make_forecasting_frame_range(self):

def test_make_forecasting_frame_pdSeries(self):

t_index = pd.date_range("1/1/2011", periods=4, freq="H")
t_index = pd.date_range("1/1/2011", periods=4, freq="h")
df, y = dataframe_functions.make_forecasting_frame(
x=pd.Series(data=range(4), index=t_index),
kind="test",
Expand All @@ -1415,7 +1416,7 @@ def test_make_forecasting_frame_pdSeries(self):

time_shifts = pd.DatetimeIndex(
["2011-01-01 01:00:00", "2011-01-01 02:00:00", "2011-01-01 03:00:00"],
freq="H",
freq="h",
)
expected_y = pd.Series(
data=[1, 2, 3], index=zip(["id"] * 3, time_shifts), name="value"
Expand Down Expand Up @@ -1451,7 +1452,7 @@ def test_make_forecasting_frame_pdSeries(self):
assert_series_equal(y, expected_y)

def test_make_forecasting_frame_feature_extraction(self):
t_index = pd.date_range("1/1/2011", periods=4, freq="H")
t_index = pd.date_range("1/1/2011", periods=4, freq="h")
df, y = dataframe_functions.make_forecasting_frame(
x=pd.Series(data=range(4), index=t_index),
kind="test",
Expand Down
6 changes: 2 additions & 4 deletions tsfresh/examples/har_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def load_har_dataset(folder_name=data_file_name):
"body_acc_x_train.txt",
)
try:
return pd.read_csv(data_file_name_dataset, delim_whitespace=True, header=None)
return pd.read_csv(data_file_name_dataset, sep=r"\s+", header=None)
except OSError:
raise OSError(
"File {} was not found. Have you downloaded the dataset with download_har_dataset() "
Expand All @@ -92,9 +92,7 @@ def load_har_classes(folder_name=data_file_name):
folder_name, "UCI HAR Dataset", "train", "y_train.txt"
)
try:
return pd.read_csv(
data_file_name_classes, delim_whitespace=True, header=None
).squeeze()
return pd.read_csv(data_file_name_classes, sep=r"\s+", header=None).squeeze()
except OSError:
raise OSError(
"File {} was not found. Have you downloaded the dataset with download_har_dataset() "
Expand Down
28 changes: 25 additions & 3 deletions tsfresh/feature_extraction/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,34 @@

def _binding_helper(f, kwargs, column_sort, column_id, column_kind, column_value):
def wrapped_feature_extraction(x):
# Capture group identity BEFORE sort_values(), because sort_values()
# returns a new DataFrame that loses the .name attribute set by groupby.apply().
# In Pandas 3.0+, grouping keys are no longer included in the chunk columns,
# so we rely on x.name (a tuple like (id_val, kind_val)) to recover them.
group_name = x.name # may be None for ungrouped DataFrames

if column_sort is not None:
x = x.sort_values(column_sort)

chunk = Timeseries(
x[column_id].iloc[0], x[column_kind].iloc[0], x[column_value]
)
if column_id in x.columns:
id_val = x[column_id].iloc[0]
elif group_name is not None:
id_val = group_name[0] if isinstance(group_name, tuple) else group_name
else:
raise KeyError(
f"Column '{column_id}' not found in DataFrame chunk and group name unavailable."
)

if column_kind in x.columns:
kind_val = x[column_kind].iloc[0]
elif group_name is not None:
kind_val = group_name[1] if isinstance(group_name, tuple) else group_name
else:
raise KeyError(
f"Column '{column_kind}' not found in DataFrame chunk and group name unavailable."
)

chunk = Timeseries(id_val, kind_val, x[column_value])
result = f(chunk, **kwargs)

result = pd.DataFrame(result, columns=[column_id, "variable", "value"])
Expand Down
14 changes: 10 additions & 4 deletions tsfresh/feature_selection/relevance.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,11 @@ def calculate_relevance_table(
if multiclass:
tmp = tmp.reset_index(drop=True)
tmp.columns = tmp.columns.map(
lambda x: x + "_" + str(label)
if x != "feature" and x != "type"
else x
lambda x: (
x + "_" + str(label)
if x != "feature" and x != "type"
else x
)
)
tables.append(tmp)

Expand Down Expand Up @@ -360,7 +362,11 @@ def infer_ml_task(y):
:return: 'classification' or 'regression'
:rtype: str
"""
if y.dtype.kind in np.typecodes["AllInteger"] or y.dtype == object:
if (
y.dtype.kind in np.typecodes["AllInteger"]
or y.dtype == object
or isinstance(y.dtype, pd.StringDtype)
):
ml_task = "classification"
else:
ml_task = "regression"
Expand Down
2 changes: 1 addition & 1 deletion tsfresh/scripts/run_tsfresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def main(console_args=None):

# Read in CSV file
input_file_name = args.input_file_name
df = pd.read_csv(input_file_name, delim_whitespace=True, header=header)
df = pd.read_csv(input_file_name, sep=r"\s+", header=header)

if not args.csv_with_headers:
df = _preprocess(df)
Expand Down
45 changes: 39 additions & 6 deletions tsfresh/utilities/dataframe_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,26 @@ def get_ids(df_or_dict, column_id):
raise TypeError("df_or_dict should be of type dict or pandas.DataFrame")


def _restore_grouping_keys(df_chunk, grouper, group_name):
"""
Restore grouping-key columns that Pandas 3.0+ no longer includes in
``groupby().apply()`` chunks. When there is more than one grouping key,
``group_name`` is a tuple whose i-th element corresponds to
``grouper[i]``; for a single key it is a scalar.

:param df_chunk: the DataFrame chunk to restore columns into
:type df_chunk: pandas.DataFrame
:param grouper: ordered list of column names used as grouping keys
:type grouper: list
:param group_name: the ``.name`` of the group object produced by
``groupby().apply()``; a tuple for multi-key groups, scalar otherwise
"""
multi_key = len(grouper) > 1
for i, col in enumerate(grouper):
if col not in df_chunk.columns:
df_chunk[col] = group_name[i] if multi_key else group_name


def _roll_out_time_series(
timeshift,
grouped_data,
Expand All @@ -279,6 +299,7 @@ def _roll_out_time_series(
min_timeshift,
column_sort,
column_id,
grouper=None,
):
"""
Internal helper function for roll_time_series.
Expand Down Expand Up @@ -334,6 +355,8 @@ def _f(x):
return

df_temp = df_temp.copy()
if grouper is not None:
_restore_grouping_keys(df_temp, grouper, x.name)

# and set the shift correctly
if column_sort and rolling_direction > 0:
Expand Down Expand Up @@ -526,15 +549,15 @@ def roll_time_series(
rolling_amount = np.abs(rolling_direction)
rolling_direction = np.sign(rolling_direction)

# Todo: not default for columns_sort to be None
if column_sort is None:
df["sort"] = range(df.shape[0])

grouped_data = df.groupby(grouper)
prediction_steps = grouped_data.count().max().max()

max_timeshift = max_timeshift or prediction_steps

# Todo: not default for columns_sort to be None
if column_sort is None:
df["sort"] = range(df.shape[0])

if rolling_direction > 0:
range_of_shifts = list(reversed(range(prediction_steps, 0, -rolling_amount)))
else:
Expand Down Expand Up @@ -563,6 +586,7 @@ def roll_time_series(
"min_timeshift": min_timeshift,
"column_sort": column_sort,
"column_id": column_id,
"grouper": grouper,
}

shifted_chunks = distributor.map_reduce(
Expand Down Expand Up @@ -721,6 +745,9 @@ def _add_id_column(df_chunk):
)
assert len(indices) == chunk_length

if grouper:
_restore_grouping_keys(df_chunk, grouper, df_chunk.name)

if column_id:
indices = list(zip(indices, df_chunk[column_id]))

Expand All @@ -732,13 +759,19 @@ def _add_id_column(df_chunk):
return df_chunk

if grouper:
df = df.groupby(grouper).apply(_add_id_column)
# include_groups=False (Pandas 2.2+): grouping columns are dropped from
# each chunk; _restore_grouping_keys() re-adds them via df_chunk.name.
# Pandas < 2.2 does not accept include_groups, so fall back gracefully.
try:
df = df.groupby(grouper).apply(_add_id_column, include_groups=False)
except TypeError:
df = df.groupby(grouper, group_keys=False).apply(_add_id_column)
else:
df = _add_id_column(df)

if column_sort:
df = df.sort_values(column_sort)

df = df.set_index(df.index.get_level_values(-1))
df.index = df.index.get_level_values(-1)

return df
Loading