Skip to content

Commit 28d01ff

Browse files
authored
♻️ polars nullable 입력 지원 (#20)
1 parent e71b09e commit 28d01ff

5 files changed

Lines changed: 45 additions & 83 deletions

File tree

.github/workflows/polars-ci.yml

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ name: Polars CI
22

33
on:
44
pull_request:
5-
types: [review_requested, ready_for_review, synchronize]
5+
types: [review_requested, ready_for_review]
66
paths:
77
- ".github/workflows/polars-*.yml"
88
- "Cargo.toml"
99
- "Makefile"
10+
- "core/**"
1011
- "polars/**"
1112

1213
concurrency:
@@ -17,82 +18,42 @@ permissions:
1718
contents: read
1819

1920
jobs:
20-
preflight:
21-
runs-on: ubuntu-latest
22-
permissions:
23-
contents: read
24-
pull-requests: read
25-
outputs:
26-
validate: ${{ steps.filter.outputs.validate }}
27-
steps:
28-
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590
29-
id: filter
30-
with:
31-
filters: |
32-
validate:
33-
- "Cargo.toml"
34-
- "Makefile"
35-
- "polars/**"
36-
3721
validate:
38-
needs: preflight
39-
if: ${{ always() }}
4022
runs-on: ubuntu-latest
4123
defaults:
4224
run:
4325
shell: bash
4426
steps:
45-
- name: Fail if preflight did not succeed
46-
if: ${{ needs.preflight.result != 'success' }}
47-
run: |
48-
echo "preflight job did not succeed: ${{ needs.preflight.result }}"
49-
exit 1
50-
51-
- name: Skip validation for non-polars changes
52-
if: ${{ needs.preflight.result == 'success' && needs.preflight.outputs.validate != 'true' }}
53-
run: echo "Skipping Polars CI validate job because this PR does not touch Cargo.toml, Makefile, or polars/**."
54-
55-
- if: ${{ needs.preflight.outputs.validate == 'true' }}
56-
uses: actions/checkout@v6
57-
- if: ${{ needs.preflight.outputs.validate == 'true' }}
58-
uses: actions/setup-python@v6
27+
- uses: actions/checkout@v6
28+
- uses: actions/setup-python@v6
5929
with:
6030
python-version: "3.10"
61-
- if: ${{ needs.preflight.outputs.validate == 'true' }}
62-
uses: astral-sh/setup-uv@v8.0.0
63-
- if: ${{ needs.preflight.outputs.validate == 'true' }}
64-
uses: dtolnay/rust-toolchain@stable
31+
- uses: astral-sh/setup-uv@v8.0.0
32+
- uses: dtolnay/rust-toolchain@stable
6533

6634
- name: Sync polars dependencies
67-
if: ${{ needs.preflight.outputs.validate == 'true' }}
6835
working-directory: polars
6936
run: uv sync --group dev --no-install-project
7037

7138
- name: Test techr-core
72-
if: ${{ needs.preflight.outputs.validate == 'true' }}
7339
run: cargo test -p techr-core
7440

7541
- name: Build local extension for tests
76-
if: ${{ needs.preflight.outputs.validate == 'true' }}
7742
working-directory: polars
7843
run: uv run maturin develop --uv
7944

8045
- name: Test polars package
81-
if: ${{ needs.preflight.outputs.validate == 'true' }}
8246
working-directory: polars
8347
run: uv run pytest
8448

8549
- name: Build wheel and sdist
86-
if: ${{ needs.preflight.outputs.validate == 'true' }}
8750
working-directory: polars
8851
run: uv run maturin build --release --sdist --out dist
8952

9053
- name: Check artifact contents
91-
if: ${{ needs.preflight.outputs.validate == 'true' }}
9254
run: python polars/scripts/check_artifacts.py polars/dist
9355

9456
- name: Smoke test built wheel
95-
if: ${{ needs.preflight.outputs.validate == 'true' }}
9657
run: |
9758
wheel="$(python - <<'PY'
9859
from pathlib import Path

polars/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "polars_techr"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
edition = "2021"
55
description = "Polars expression plugins for techr indicators"
66
license = "MIT"

polars/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ result = df.select(
4545
)
4646
```
4747

48+
Null input values are accepted. Indicators preserve row alignment and emit nulls according to the core rolling-window and seed recovery rules.
49+
4850
## Ichimoku Notes
4951

5052
- Standalone Ichimoku rolling-window lines such as `ichimoku_base_line` and `ichimoku_conversion_line` use `period`.

polars/src/expressions.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,9 @@ struct IchimokuLaggingSpanKwargs {
6969
base_line_period: u32,
7070
}
7171

72-
fn series_to_f64_vec(series: &Series) -> PolarsResult<Vec<f64>> {
72+
fn series_to_f64_vec(series: &Series) -> PolarsResult<Vec<Option<f64>>> {
7373
let casted = series.cast(&DataType::Float64)?;
74-
let values = casted.f64()?.to_vec_null_aware();
75-
if let Some(values) = values.left() {
76-
Ok(values)
77-
} else {
78-
Err(PolarsError::ComputeError(
79-
"null values are not supported yet".into(),
80-
))
81-
}
74+
Ok(casted.f64()?.to_vec())
8275
}
8376

8477
fn option_vec_to_series(values: Vec<Option<f64>>) -> Series {

polars/tests/test_indicators.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -284,41 +284,47 @@ def test_multi_input_integer_columns_are_cast_to_float() -> None:
284284
])
285285

286286

287-
def test_single_input_null_values_raise_compute_error() -> None:
288-
"""Reject null values for single-input indicators with a Polars compute error."""
287+
@pytest.mark.parametrize("lazy", [False, True])
288+
def test_single_input_null_values_follow_core_gap_semantics(lazy: bool) -> None:
289+
"""Accept null values for single-input indicators."""
289290
# given
290-
df = pl.DataFrame({"close": [1.0, None, 3.0]})
291+
df = pl.DataFrame({"close": [1.0, None, 3.0, 4.0]})
292+
293+
# when
294+
result = select_expr(df, ta.sma(pl.col("close"), period=2), "sma", lazy)
291295

292-
# when / then
293-
with pytest.raises(
294-
pl.exceptions.ComputeError,
295-
match="null values are not supported yet",
296-
):
297-
df.select(ta.sma(pl.col("close"), period=2).alias("sma"))
296+
# then
297+
assert_values_close(result.to_list(), [None, None, None, 3.5])
298298

299299

300-
def test_multi_input_null_values_raise_compute_error() -> None:
301-
"""Reject null values for multi-input indicators with a Polars compute error."""
300+
@pytest.mark.parametrize("lazy", [False, True])
301+
def test_multi_input_null_values_follow_core_gap_semantics(lazy: bool) -> None:
302+
"""Accept null values for multi-input indicators."""
302303
# given
303304
df = pl.DataFrame(
304305
{
305-
"high": [11.0, 12.0, None],
306-
"low": [1.0, 2.0, 3.0],
307-
"close": [6.0, 7.0, 8.0],
306+
"high": [5.0, 7.0, None, 10.0, 12.0],
307+
"low": [1.0, 3.0, None, 6.0, 8.0],
308+
"close": [4.0, 5.0, None, 8.0, 11.0],
308309
}
309310
)
310311

311-
# when / then
312-
with pytest.raises(
313-
pl.exceptions.ComputeError,
314-
match="null values are not supported yet",
315-
):
316-
df.select(
317-
ta.stochf_percent_k(
318-
pl.col("high"),
319-
pl.col("low"),
320-
pl.col("close"),
321-
fastk_period=3,
322-
fastd_period=2,
323-
).alias("value")
324-
)
312+
# when
313+
result = select_expr(
314+
df,
315+
ta.stochf_percent_k(
316+
pl.col("high"),
317+
pl.col("low"),
318+
pl.col("close"),
319+
fastk_period=2,
320+
fastd_period=2,
321+
),
322+
"value",
323+
lazy,
324+
)
325+
326+
# then
327+
assert_values_close(
328+
result.to_list(),
329+
[None, 66.66666666666666, None, None, 83.33333333333334],
330+
)

0 commit comments

Comments
 (0)