Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions include/LightGBM/c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -563,11 +563,11 @@ LIGHTGBM_C_EXPORT int LGBM_DatasetDumpText(DatasetHandle handle,
/*!
* \brief Set vector to a content in info.
* \note
* - \a group only works for ``C_API_DTYPE_INT32``;
* - \a group and \a position only work for ``C_API_DTYPE_INT32``;
* - \a label and \a weight only work for ``C_API_DTYPE_FLOAT32``;
* - \a init_score only works for ``C_API_DTYPE_FLOAT64``.
* \param handle Handle of dataset
* \param field_name Field name, can be \a label, \a weight, \a init_score, \a group
* \param field_name Field name, can be \a label, \a weight, \a init_score, \a group, \a position
* \param field_data Pointer to data vector
* \param num_element Number of elements in ``field_data``
* \param type Type of ``field_data`` pointer, can be ``C_API_DTYPE_INT32``, ``C_API_DTYPE_FLOAT32`` or ``C_API_DTYPE_FLOAT64``
Expand All @@ -583,11 +583,11 @@ LIGHTGBM_C_EXPORT int LGBM_DatasetSetField(DatasetHandle handle,
* \brief Set vector to a content in info.
* \deprecated This function is deprecated in favor of ``LGBM_DatasetSetFieldFromArrowStream``.
* \note
* - \a group converts input datatype into ``int32``;
* - \a group and \a position convert input datatype into ``int32``;
* - \a label and \a weight convert input datatype into ``float32``;
* - \a init_score converts input datatype into ``float64``.
* \param handle Handle of dataset
* \param field_name Field name, can be \a label, \a weight, \a init_score, \a group
* \param field_name Field name, can be \a label, \a weight, \a init_score, \a group, \a position
* \param n_chunks The number of Arrow arrays passed to this function
* \param chunks Pointer to the list of Arrow arrays
* \param schema Pointer to the schema of all Arrow arrays
Expand Down
2 changes: 2 additions & 0 deletions include/LightGBM/dataset.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class Metadata {
void SetQuery(int64_t n_chunks, struct ArrowArray* chunks, struct ArrowSchema* schema);

void SetPosition(const data_size_t* position, data_size_t len);
void SetPosition(struct ArrowArrayStream* stream);
void SetPosition(int64_t n_chunks, struct ArrowArray* chunks, struct ArrowSchema* schema);

/*!
* \brief Set initial scores
Expand Down
14 changes: 9 additions & 5 deletions python-package/lightgbm/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,12 @@
pd_Series,
nwt.IntoSeries,
]
# 'position' intentionally does not support 'list' inputs
# ref: https://github.com/lightgbm-org/LightGBM/pull/5929#discussion_r1262646998
_LGBM_PositionType = Union[
np.ndarray,
pd_Series,
nwt.IntoSeries,
]
_LGBM_InitScoreType = Union[
List[float],
Expand Down Expand Up @@ -1752,7 +1755,7 @@ def __init__(
Other parameters for Dataset.
free_raw_data : bool, optional (default=True)
If True, raw data is freed after constructing inner Dataset.
position : numpy 1-D array, pandas Series or None, optional (default=None)
position : numpy 1-D array, pandas Series, pyarrow ChunkedArray, polars Series or None, optional (default=None)
Position of items used in unbiased learning-to-rank task.
"""
self._handle: Optional[_DatasetHandle] = None
Expand Down Expand Up @@ -2577,7 +2580,7 @@ def create_valid(
Init score for Dataset.
params : dict or None, optional (default=None)
Other parameters for validation Dataset.
position : numpy 1-D array, pandas Series or None, optional (default=None)
position : numpy 1-D array, pandas Series, pyarrow ChunkedArray, polars Series or None, optional (default=None)
Position of items used in unbiased learning-to-rank task.

Returns
Expand Down Expand Up @@ -3102,7 +3105,7 @@ def set_position(

Parameters
----------
position : numpy 1-D array, pandas Series or None, optional (default=None)
position : numpy 1-D array, pandas Series, pyarrow ChunkedArray, polars Series or None, optional (default=None)
Position of items used in unbiased learning-to-rank task.

Returns
Expand All @@ -3112,7 +3115,8 @@ def set_position(
"""
self.position = position
if self._handle is not None and position is not None:
position = _list_to_1d_numpy(data=position, dtype=np.int32, name="position")
if isinstance(position, pd_Series) or not nwd.is_into_series(position):
position = _list_to_1d_numpy(data=position, dtype=np.int32, name="position")
self.set_field("position", position)
self.position = self.get_field("position") # original values can be modified at cpp side
return self
Expand Down Expand Up @@ -3262,7 +3266,7 @@ def get_position(self) -> Optional[_LGBM_PositionType]:

Returns
-------
position : numpy 1-D array, pandas Series or None
position : numpy 1-D array, pandas Series, pyarrow ChunkedArray, polars Series or None
Position of items used in unbiased learning-to-rank task.
For a constructed ``Dataset``, this will only return ``None`` or a numpy array.
"""
Expand Down
4 changes: 4 additions & 0 deletions src/io/dataset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,8 @@ bool Dataset::SetFieldFromArrow(const char* field_name, struct ArrowArrayStream*
metadata_.SetInitScore(stream);
} else if (name == std::string("query") || name == std::string("group")) {
metadata_.SetQuery(stream);
} else if (name == std::string("position")) {
metadata_.SetPosition(stream);
} else {
return false;
}
Expand All @@ -959,6 +961,8 @@ bool Dataset::SetFieldFromArrow(const char* field_name, int64_t n_chunks,
metadata_.SetInitScore(n_chunks, chunks, schema);
} else if (name == std::string("query") || name == std::string("group")) {
metadata_.SetQuery(n_chunks, chunks, schema);
} else if (name == std::string("position")) {
metadata_.SetPosition(n_chunks, chunks, schema);
} else {
return false;
}
Expand Down
27 changes: 27 additions & 0 deletions src/io/metadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,33 @@ void Metadata::SetPosition(const data_size_t* positions, data_size_t len) {
}
}

#ifndef LGB_R_BUILD
void Metadata::SetPosition(struct ArrowArrayStream* stream) {
ArrowChunkedArray chunked_array(stream);
chunked_array.view().visit<data_size_t>([&](auto&& visitor) {
std::vector<data_size_t> positions;
positions.reserve(visitor.end() - visitor.begin());
for (auto it = visitor.begin(); it != visitor.end(); ++it) {
positions.push_back(*it);
}
SetPosition(positions.data(), static_cast<data_size_t>(positions.size()));
});
}

void Metadata::SetPosition(int64_t n_chunks, struct ArrowArray* chunks,
struct ArrowSchema* schema) {
ArrowChunkedArray chunked_array(n_chunks, chunks, schema);
chunked_array.view().visit<data_size_t>([&](auto&& visitor) {
std::vector<data_size_t> positions;
positions.reserve(visitor.end() - visitor.begin());
for (auto it = visitor.begin(); it != visitor.end(); ++it) {
positions.push_back(*it);
}
SetPosition(positions.data(), static_cast<data_size_t>(positions.size()));
});
}
#endif // LGB_R_BUILD

void Metadata::InsertQueries(const data_size_t* queries, data_size_t start_index, data_size_t len) {
if (!queries) {
Log::Fatal("Passed null queries");
Expand Down
36 changes: 36 additions & 0 deletions tests/python_package_test/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,42 @@ def test_dataset_construct_groups(group_data, arrow_type):
np_assert_array_equal(expected, dataset.get_field("group"), strict=True)


# ------------------------------------------ POSITION ------------------------------------------- #


@pytest.mark.parametrize(
"position_data",
[
[[0, 1, 2, 3, 4]],
[[0, 1, 2], [3, 4]],
[[], [0, 1, 2], [3, 4]],
[[0, 1], [], [2], [3, 4], []],
],
)
@pytest.mark.parametrize("arrow_type", _INTEGER_TYPES)
def test_dataset_construct_position(position_data, arrow_type):
data = generate_dummy_arrow_table()
positions = pa.chunked_array(position_data, type=arrow_type)
dataset = lgb.Dataset(data, label=[0, 1, 0, 1, 0], position=positions, params=dummy_dataset_params())
dataset.construct()

expected = np.array([0, 1, 2, 3, 4], dtype=np.int32)
np_assert_array_equal(expected, dataset.get_field("position"), strict=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test_dataset_construct_position() test looks great.

Could you please add one more test here ensuring that cases where position has duplicate or out-of-order values are handled correctly? I don't see a bug in your changes, but could imagine cases where some logic bug would produce the values 0, 1, 2, 3, 4 for a 5-element position even when that encoding is not correct.

A test case like position=pa.chunked_array([15, 15, 15, 27, 8, 9, 15]) or something would be a strong test of correctness. It'd also be a way to cover that difference between get_field("position") and .position that you called out in a comment in the set_position() body.

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.

Added a unit test that addresses these cases! 👍🏻



@pytest.mark.parametrize("arrow_type", _INTEGER_TYPES)
def test_dataset_construct_position_with_duplicates_and_out_of_order(arrow_type):
data = generate_dummy_arrow_table()
positions = pa.chunked_array([[15, 15, 8, 27, 15]], type=arrow_type)
dataset = lgb.Dataset(data, label=[0, 1, 0, 1, 0], position=positions, params=dummy_dataset_params())
dataset.construct()

# positions are remapped on the C++ side to dense indices in first-seen order:
# 15 -> 0, 8 -> 1, 27 -> 2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Love this description of how this works, thank you.

expected = np.array([0, 0, 1, 2, 0], dtype=np.int32)
np_assert_array_equal(expected, dataset.get_field("position"), strict=True)


# ----------------------------------------- INIT SCORES ----------------------------------------- #


Expand Down
27 changes: 27 additions & 0 deletions tests/python_package_test/test_polars.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,33 @@ def test_dataset_construct_groups(polars_type):
np_assert_array_equal(expected, dataset.get_field("group"), strict=True)


# ------------------------------------------ POSITION ------------------------------------------- #


@pytest.mark.parametrize("polars_type", _INTEGER_TYPES)
def test_dataset_construct_position(polars_type):
data = generate_dummy_polars_frame()
positions = pl.Series("position", [0, 1, 2, 3, 4], dtype=polars_type)
dataset = lgb.Dataset(data, label=[0, 1, 0, 1, 0], position=positions, params=dummy_dataset_params())
dataset.construct()

expected = np.array([0, 1, 2, 3, 4], dtype=np.int32)
np_assert_array_equal(expected, dataset.get_field("position"), strict=True)


@pytest.mark.parametrize("polars_type", _INTEGER_TYPES)
def test_dataset_construct_position_with_duplicates_and_out_of_order(polars_type):
data = generate_dummy_polars_frame()
positions = pl.Series("position", [15, 15, 8, 27, 15], dtype=polars_type)
dataset = lgb.Dataset(data, label=[0, 1, 0, 1, 0], position=positions, params=dummy_dataset_params())
dataset.construct()

# positions are remapped on the C++ side to dense indices in first-seen order:
# 15 -> 0, 8 -> 1, 27 -> 2
expected = np.array([0, 0, 1, 2, 0], dtype=np.int32)
np_assert_array_equal(expected, dataset.get_field("position"), strict=True)


# ----------------------------------------- INIT SCORES ----------------------------------------- #


Expand Down
Loading