datasentry-core 0.8.2__tar.gz → 0.8.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/.gitignore +6 -0
  2. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/PKG-INFO +1 -1
  3. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/pyproject.toml +1 -1
  4. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/csv.py +17 -0
  5. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/missing.py +4 -2
  6. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/numeric.py +8 -5
  7. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/missingness.py +4 -2
  8. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/engine/__init__.py +2 -1
  9. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/engine/base.py +12 -0
  10. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/engine/profiler.py +22 -7
  11. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/scan.py +1 -1
  12. datasentry_core-0.8.4/src/datasentry_core/repair/engine.py +852 -0
  13. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/i18n.py +16 -0
  14. datasentry_core-0.8.2/src/datasentry_core/repair/engine.py +0 -498
  15. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/README.md +0 -0
  16. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/__init__.py +0 -0
  17. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/__init__.py +0 -0
  18. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/base.py +0 -0
  19. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/duckdb.py +0 -0
  20. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/errors.py +0 -0
  21. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/file_based.py +0 -0
  22. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/jsonl.py +0 -0
  23. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/mysql.py +0 -0
  24. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/parquet.py +0 -0
  25. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/postgres.py +0 -0
  26. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/registry.py +0 -0
  27. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/remote_file.py +0 -0
  28. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/sampling.py +0 -0
  29. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/spec.py +0 -0
  30. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/sqlite.py +0 -0
  31. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/connectors/xlsx.py +0 -0
  32. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/contracts/__init__.py +0 -0
  33. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/contracts/exporters.py +0 -0
  34. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/__init__.py +0 -0
  35. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/base.py +0 -0
  36. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/common.py +0 -0
  37. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/cross_field.py +0 -0
  38. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/cross_table.py +0 -0
  39. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/datetime.py +0 -0
  40. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/__init__.py +0 -0
  41. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/anomaly_ml.py +0 -0
  42. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/categorical.py +0 -0
  43. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/formula.py +0 -0
  44. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/textual.py +0 -0
  45. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/initial/uniqueness.py +0 -0
  46. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/runner.py +0 -0
  47. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/safe_eval.py +0 -0
  48. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/detectors/textual_variants.py +0 -0
  49. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/drift/__init__.py +0 -0
  50. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/drift/engine.py +0 -0
  51. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/engine/duckdb.py +0 -0
  52. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/engine/fusion.py +0 -0
  53. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/engine/sql_guard.py +0 -0
  54. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/llm/provider.py +0 -0
  55. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/__init__.py +0 -0
  56. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/audit.py +0 -0
  57. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/contract.py +0 -0
  58. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/detector.py +0 -0
  59. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/drift.py +0 -0
  60. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/enums.py +0 -0
  61. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/evidence.py +0 -0
  62. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/feedback.py +0 -0
  63. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/fingerprint.py +0 -0
  64. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/issue.py +0 -0
  65. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/llm.py +0 -0
  66. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/profile.py +0 -0
  67. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/quality.py +0 -0
  68. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/repair.py +0 -0
  69. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/rules.py +0 -0
  70. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/models/validation.py +0 -0
  71. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/plugin_fixtures.py +0 -0
  72. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/plugin_locks.py +0 -0
  73. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/plugins.py +0 -0
  74. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/privacy/redactor.py +0 -0
  75. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/repair/__init__.py +0 -0
  76. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/__init__.py +0 -0
  77. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/column_profiles.py +0 -0
  78. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/evidence_desc.py +0 -0
  79. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/html.py +0 -0
  80. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/interactive.py +0 -0
  81. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/junit.py +0 -0
  82. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/markdown.py +0 -0
  83. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/sarif.py +0 -0
  84. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/suggestions.py +0 -0
  85. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/reporting/translate.py +0 -0
  86. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/rules/engine.py +0 -0
  87. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/scoring/__init__.py +0 -0
  88. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/scoring/engine.py +0 -0
  89. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/scoring/gate.py +0 -0
  90. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/scoring/quality.py +0 -0
  91. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/scoring/weights.py +0 -0
  92. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/secrets.py +0 -0
  93. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/storage/__init__.py +0 -0
  94. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/storage/paths.py +0 -0
  95. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/storage/schema.py +0 -0
  96. {datasentry_core-0.8.2 → datasentry_core-0.8.4}/src/datasentry_core/storage/store.py +0 -0
@@ -41,3 +41,9 @@ docs/V*_DEV_PROMPT.md
41
41
  DataSentry AI 产品设计与开发 Prompt*.md
42
42
  DataSentry AI 产品设计与开发 Prompt*.docx
43
43
  DataSentry AI 增长与推广 Prompt.md
44
+
45
+ # Prompt 与体检报告所在目录(本地保留,不进仓库)
46
+ AI Data Quality Copilot/
47
+
48
+ # 审计产物(本地保留,不进仓库;阶段 4 指定位置)
49
+ AUDIT/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datasentry-core
3
- Version: 0.8.2
3
+ Version: 0.8.4
4
4
  Summary: DataSentry AI core domain models and engine (detectors, fusion, scoring, repairs).
5
5
  License: Apache-2.0
6
6
  Keywords: data-observability,data-quality,duckdb
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datasentry-core"
3
- version = "0.8.2"
3
+ version = "0.8.4"
4
4
  description = "DataSentry AI core domain models and engine (detectors, fusion, scoring, repairs)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -79,6 +79,23 @@ def _sniff_delimiter(path: Path, encoding: str) -> str:
79
79
  return ","
80
80
 
81
81
 
82
+ def sniff_profile(path: Path) -> tuple[str, str]:
83
+ """The (encoding, delimiter) this connector would use for `path`.
84
+
85
+ Public for exactly one reason: the repair auditor reads the artefacts, and reading them under
86
+ a different encoding than the bytes are in is not a smaller lie than reading them under a
87
+ different delimiter. A cp1250 snapshot raised `invalid UTF8 data` and the repair lost its audit
88
+ evidence while `apply` had already succeeded (F2).
89
+ """
90
+ encoding = _detect_encoding(path)
91
+ return encoding, _sniff_delimiter(path, encoding)
92
+
93
+
94
+ def sniff_dialect(path: Path) -> str:
95
+ """The delimiter this connector would use for `path`."""
96
+ return sniff_profile(path)[1]
97
+
98
+
82
99
  def _schema_hash(column_signature: list[tuple[str, str]]) -> str:
83
100
  """schema_hash = 列签名规范 JSON 的 SHA-256(19.1)。"""
84
101
  canonical = json.dumps(column_signature, sort_keys=True, ensure_ascii=True)
@@ -54,8 +54,10 @@ class ExcessiveNullRateDetector(DetectorBase):
54
54
  table = context.handle.sql_aggregate(
55
55
  f"SELECT count(*) AS n, sum({q} IS NULL) AS nulls FROM data"
56
56
  ).table
57
- total = int(table.column("n").to_pylist()[0])
58
- nulls = int(table.column("nulls").to_pylist()[0])
57
+ raw_total = table.column("n").to_pylist()[0]
58
+ raw_nulls = table.column("nulls").to_pylist()[0]
59
+ total = int(raw_total) if raw_total is not None else 0
60
+ nulls = int(raw_nulls) if raw_nulls is not None else 0
59
61
  if total <= 0:
60
62
  continue
61
63
  ratio = nulls / total
@@ -12,6 +12,7 @@ from datasentry_core.detectors.common import (
12
12
  numeric_columns,
13
13
  quote_ident,
14
14
  )
15
+ from datasentry_core.engine.base import finite_only
15
16
  from datasentry_core.models.detector import DetectorCapabilities, IssueCandidate
16
17
  from datasentry_core.models.enums import EvidenceType, QualityDimension, Severity
17
18
  from datasentry_core.reporting.evidence_desc import ev
@@ -33,7 +34,8 @@ class IqrOutlierDetector(DetectorBase):
33
34
  for col in numeric_columns(context):
34
35
  q = quote_ident(col)
35
36
  stat = context.handle.sql_aggregate(
36
- f"SELECT quantile_cont({q}, 0.25) AS q25, quantile_cont({q}, 0.75) AS q75 FROM data"
37
+ f"SELECT quantile_cont({finite_only(q)}, 0.25) AS q25, "
38
+ f"quantile_cont({finite_only(q)}, 0.75) AS q75 FROM data"
37
39
  ).table
38
40
  row = stat.to_pylist()[0]
39
41
  q25, q75 = row["q25"], row["q75"]
@@ -102,7 +104,7 @@ class ModifiedZScoreDetector(DetectorBase):
102
104
  for col in numeric_columns(context):
103
105
  q = quote_ident(col)
104
106
  stat = context.handle.sql_aggregate(
105
- f"SELECT median({q}) AS m, mad({q}) AS mad FROM data"
107
+ f"SELECT median({finite_only(q)}) AS m, mad({finite_only(q)}) AS mad FROM data"
106
108
  ).table
107
109
  row = stat.to_pylist()[0]
108
110
  median, mad = row["m"], row["mad"]
@@ -211,8 +213,8 @@ class PercentileOutlierDetector(DetectorBase):
211
213
  for col in numeric_columns(context):
212
214
  q = quote_ident(col)
213
215
  stat = context.handle.sql_aggregate(
214
- f"SELECT quantile_cont({q}, 0.001) AS p_low, quantile_cont({q}, 0.999) AS p_high "
215
- f"FROM data"
216
+ f"SELECT quantile_cont({finite_only(q)}, 0.001) AS p_low, "
217
+ f"quantile_cont({finite_only(q)}, 0.999) AS p_high FROM data"
216
218
  ).table
217
219
  row = stat.to_pylist()[0]
218
220
  p_low, p_high = row["p_low"], row["p_high"]
@@ -271,7 +273,8 @@ class HistogramRarityDetector(DetectorBase):
271
273
  for col in numeric_columns(context):
272
274
  q = quote_ident(col)
273
275
  stat = context.handle.sql_aggregate(
274
- f"SELECT min({q}) AS lo, max({q}) AS hi, count({q}) AS n FROM data"
276
+ f"SELECT min({finite_only(q)}) AS lo, max({finite_only(q)}) AS hi, "
277
+ f"count({q}) AS n FROM data"
275
278
  ).table
276
279
  row = stat.to_pylist()[0]
277
280
  lo, hi, total = row["lo"], row["hi"], row["n"]
@@ -61,8 +61,10 @@ def _column_nulls(context: DetectionContext, column: str) -> tuple[int, int]:
61
61
  table = context.handle.sql_aggregate(
62
62
  f"SELECT count(*) AS n, sum({q} IS NULL) AS nulls FROM data"
63
63
  ).table
64
- total = int(table.column("n").to_pylist()[0])
65
- nulls = int(table.column("nulls").to_pylist()[0])
64
+ raw_total = table.column("n").to_pylist()[0]
65
+ raw_nulls = table.column("nulls").to_pylist()[0]
66
+ total = int(raw_total) if raw_total is not None else 0
67
+ nulls = int(raw_nulls) if raw_nulls is not None else 0
66
68
  return total, nulls
67
69
 
68
70
 
@@ -1,6 +1,6 @@
1
1
  """执行层:Step 3 DuckDB 只读执行器 + Step 4 Profiling + Step 7 证据融合(Polars 归 V1)。"""
2
2
 
3
- from datasentry_core.engine.base import SqlExecutor, SqlParams
3
+ from datasentry_core.engine.base import SqlExecutor, SqlParams, finite_only
4
4
  from datasentry_core.engine.duckdb import DuckDBExecutor
5
5
  from datasentry_core.engine.fusion import EvidenceFusionEngine
6
6
  from datasentry_core.engine.profiler import Profiler
@@ -13,4 +13,5 @@ __all__ = [
13
13
  "SqlExecutor",
14
14
  "SqlParams",
15
15
  "assert_read_only_sql",
16
+ "finite_only",
16
17
  ]
@@ -59,3 +59,15 @@ class SetupExecutor(Protocol):
59
59
  ) -> Iterator[pa.RecordBatch]: ...
60
60
 
61
61
  def close(self) -> None: ...
62
+
63
+
64
+ def finite_only(quoted: str) -> str:
65
+ """The expression naming only the finite values of a numeric column.
66
+
67
+ NaN and ±Inf are values -- they still count and they are not NULL -- but they are not numbers.
68
+ Unguarded, DuckDB's `stddev` aborts the whole aggregate (`OutOfRangeException`), `max([1.0,
69
+ nan, 3.0])` answers `nan`, a median orders NaN as a value, and a detector that splices a NaN
70
+ bound into a predicate fails with `Referenced column "nan" not found`. One rule for the
71
+ profiler and for every detector, so the same column cannot be summarised two ways (G-2).
72
+ """
73
+ return f"CASE WHEN isfinite({quoted}) THEN {quoted} END"
@@ -6,6 +6,8 @@
6
6
  约定:
7
7
  - 数值列(INTEGER/FLOAT/DOUBLE/DECIMAL…)计算 min/max/mean/std/q25/median/q75;
8
8
  字符串列仅统计 distinct/min/max;其余类型取 min/max
9
+ - 数值列的统计量只在**有限值**上计算:NaN/±Inf 仍算一个"值"(进 count 与 distinct、
10
+ 不算 NULL),但不进 min/max/mean/std/分位(G-2:不守卫则 stddev 直接抛,整次扫描失败)
9
11
  - 空字符串在 duckdb 视图路径已被折叠为 NULL(见 CSV 语义约定),null 统计包含空串
10
12
  - examples 字段刻意留空:脱敏设施在 Step 15(LLM Provider 抽象)之后提供,
11
13
  在此之前避免把原始样本写入画像输出
@@ -19,6 +21,7 @@ from typing import Any
19
21
  import pyarrow as pa
20
22
 
21
23
  from datasentry_core.connectors.base import DataHandle
24
+ from datasentry_core.engine.base import finite_only
22
25
  from datasentry_core.models.profile import ColumnProfile, DatasetProfile
23
26
 
24
27
  _NUMERIC_TYPES = frozenset(
@@ -46,16 +49,28 @@ def _column_exprs(column: str, quoted: str, numeric: bool) -> list[str]:
46
49
  exprs = [
47
50
  f"count({quoted}) AS {alias}",
48
51
  f"count(DISTINCT {quoted}) AS {_quote_ident(f'{column}__distinct')}",
49
- f"min({quoted}) AS {_quote_ident(f'{column}__min')}",
50
- f"max({quoted}) AS {_quote_ident(f'{column}__max')}",
51
52
  ]
52
53
  if numeric:
54
+ # NaN and ±Inf are values (they still count) but they are not numbers. Left unguarded,
55
+ # `stddev` aborts the whole aggregate -- `OutOfRangeException: STDDEV_SAMP is out of
56
+ # range!` -- so `scan_file` fails on an ordinary CSV whose numeric column carries the token
57
+ # `nan`, which is what pandas-style exports produce; and the aggregates that do not abort
58
+ # answer wrongly (`max([1.0, nan, 3.0])` is `nan`, the median becomes `3.0`). Statistics are
59
+ # therefore taken over the finite values only (G-2).
60
+ stat = finite_only(quoted)
53
61
  exprs += [
54
- f"avg({quoted}) AS {_quote_ident(f'{column}__mean')}",
55
- f"stddev({quoted}) AS {_quote_ident(f'{column}__std')}",
56
- f"quantile_cont({quoted}, 0.25) AS {_quote_ident(f'{column}__q25')}",
57
- f"quantile_cont({quoted}, 0.5) AS {_quote_ident(f'{column}__median')}",
58
- f"quantile_cont({quoted}, 0.75) AS {_quote_ident(f'{column}__q75')}",
62
+ f"min({stat}) AS {_quote_ident(f'{column}__min')}",
63
+ f"max({stat}) AS {_quote_ident(f'{column}__max')}",
64
+ f"avg({stat}) AS {_quote_ident(f'{column}__mean')}",
65
+ f"stddev({stat}) AS {_quote_ident(f'{column}__std')}",
66
+ f"quantile_cont({stat}, 0.25) AS {_quote_ident(f'{column}__q25')}",
67
+ f"quantile_cont({stat}, 0.5) AS {_quote_ident(f'{column}__median')}",
68
+ f"quantile_cont({stat}, 0.75) AS {_quote_ident(f'{column}__q75')}",
69
+ ]
70
+ else:
71
+ exprs += [
72
+ f"min({quoted}) AS {_quote_ident(f'{column}__min')}",
73
+ f"max({quoted}) AS {_quote_ident(f'{column}__max')}",
59
74
  ]
60
75
  return exprs
61
76
 
@@ -21,7 +21,7 @@ class SamplingConfig(BaseModel):
21
21
 
22
22
  method: Literal[
23
23
  "random", "stratified", "reservoir", "time_based", "rare_oversampling", "none"
24
- ] = "random"
24
+ ] = "reservoir"
25
25
  sample_size: int | None = Field(default=None, ge=1)
26
26
  ratio: float | None = Field(default=None, gt=0.0, le=1.0)
27
27
  seed: int = 42