mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
一份快照按期次分组的变化拆解也核本期齐不齐(复核 R20):可加、比率、乘积三种模式用波动基线同一套完整性检查(comparable.period_complete:不到整周、没过完、数据没到、SQL 截掉一部分),本期不齐算不了(partial_target,「本期数据只到 2026-10-20,还差 5 天,和完整的周比没有意义」),核对不了(查不到表的范围、周号认不出算法、期次不是日期)附一句说明,只在证据面板、不进判定语(两份快照时可比性结论照旧进判定语);结论的 params 记下运行当天。期次按 periods.parse_compared 认:周号按 SQL 认算法;快照里往往只有比的两期,值都是日期、推断成按周、期次列又是直接取的日期列(原样、date(列)、DATE(列)、CAST 成日期)时按天认——昨天对上周同一天正好隔 7 天,光看值分不出是两天还是两周。可比性检查按期次分组、没给粒度时同样按它认。证据接口的一条测试(tests/test_analysis_evidence_api.py)跟着补上这一句说明的种类
This commit is contained in:
@@ -568,7 +568,8 @@ def comparable(raw: Mapping[str, Any], env: Any) -> Any:
|
||||
"里有的期次值",
|
||||
reason=f"期次列「{period}」里没有{what}「{ph.key_text([params[side]])}」")
|
||||
env.inputs.use(target, "data", [period, *([measure] if measure else [])], rows["base"] + rows["target"])
|
||||
parsed = pd.parse(values, grain, weeks=pd.week_scheme(target.sql, period, target.dialect))
|
||||
parsed = pd.parse(values, grain, weeks=pd.week_scheme(target.sql, period, target.dialect)) if grain \
|
||||
else pd.parse_compared(values, target.sql, period, target.dialect)
|
||||
if parsed is None:
|
||||
item_b = item_t = pd.Period(None, 0, None, None)
|
||||
grain = "seq"
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
**参数**(AnalysisRequest.params;列名不分大小写,按快照认):
|
||||
|
||||
- 一份快照:query(编号)、period(期次列)、base / target(期次列里的两个值);或两份快照:base_query、target_query(各自
|
||||
只有维度列和度量列,先做可比性检查:口径结构不同的不拆,算不了 incomparable_sides;其余不可比的照样算,判定后附上
|
||||
可比性结论)。
|
||||
- 一份快照:query(编号)、period(期次列)、base / target(期次列里的两个值),本期齐不齐同波动基线那一套(comparable.
|
||||
period_complete:不到整期、没过完、数据没到 → partial_target;核对不了的附说明,只在证据面板);或两份快照:base_query、
|
||||
target_query(各自只有维度列和度量列,先做可比性检查:口径结构不同的不拆,算不了 incomparable_sides;其余不可比的照样算,
|
||||
判定后附上可比性结论)。
|
||||
- additive:by(维度列,1–2 列)、measure(度量列);可选 headline {base, target}(基期、本期总数各一格,如 Q1.r1.gmv,用来
|
||||
核对闭合)、parent {of: A3, key: [华东]}(下钻:这一层各段之和要等于 A3 里华东那一段)。
|
||||
- ratio:by、numerator(分子列)、denominator(分母列);可选 headline {base, target}(整体比率的两格)、parent(上一层的比率
|
||||
@@ -275,6 +276,30 @@ def _comparability(env: Env, params: dict[str, Any], base: Snap, target: Snap, m
|
||||
return outcome, outcome.structure
|
||||
|
||||
|
||||
def _period_complete(env: Env, params: dict[str, Any], snap: Snap, measure: str | None
|
||||
) -> tuple[str | None, dict[str, str] | None]:
|
||||
"""一份快照按期次分组时本期齐不齐(同波动基线那一套,comparable.period_complete;期次按 periods.parse_compared 认):
|
||||
(不齐时 partial_target 的原因, 核对不了时的附带说明)。两份快照的在可比性检查里核过整期,不在这里核。"""
|
||||
from app.analysis import periods as pd
|
||||
|
||||
if "base_query" in params:
|
||||
return None, None
|
||||
params["today"] = today_param(env)
|
||||
period = params["period"]
|
||||
i = snap.index(period)
|
||||
values = [row[i] if i < len(row) else None for row in snap.rows]
|
||||
parsed = pd.parse_compared(values, snap.sql, period, snap.dialect)
|
||||
if parsed is None:
|
||||
return None, ph.caveat("comparability", ph.NO_DATES)
|
||||
items, grain = parsed
|
||||
target = next(item for item, v in zip(items, values) if same_value(params["target"], v))
|
||||
status, why, note = cmp.period_complete(snap, period, target, grain, measure, env.facts, env.book,
|
||||
env.today) # type: ignore[arg-type]
|
||||
if status == "short":
|
||||
return f"{why},和完整的{pd.PER_WORD[grain]}比没有意义", None
|
||||
return None, ph.caveat("comparability", note) if status == "unknown" and note else None
|
||||
|
||||
|
||||
def _share_rank(segments: Sequence[Any], share: Any) -> list[Any]:
|
||||
"""按 share 从大到小(一样大的按先出现的)。"""
|
||||
return sorted(segments, key=lambda s: -share(s))
|
||||
@@ -323,6 +348,12 @@ def additive(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
if structure is not None:
|
||||
return refuse(env, params, used, "incomparable_sides", f"{structure},不是同一个量的两期,不拆",
|
||||
criterion=ph.criterion_additive(env.settings, top_k_max=None, **criterion_args), caveats=caveats)
|
||||
partial, note = _period_complete(env, params, ts, measure)
|
||||
if partial is not None:
|
||||
return refuse(env, params, used, "partial_target", partial,
|
||||
criterion=ph.criterion_additive(env.settings, top_k_max=None, **criterion_args), caveats=caveats)
|
||||
if note is not None:
|
||||
caveats.append(note)
|
||||
segs = _segments(side_b, side_t, Fraction(0))
|
||||
if len(segs) < 2:
|
||||
return refuse(env, params, used, "too_few_segments",
|
||||
@@ -435,7 +466,9 @@ def _additive_result(env: Env, params: dict[str, Any], used: list[str], segs: li
|
||||
values["gap"] = canon(max(gaps.values(), key=abs))
|
||||
verdict = {"code": code, "subjects": [_key_strings(x.key) for x in subjects],
|
||||
"direction": sign_word(delta) if code in ("driver", "concentrated", "dispersed") else None}
|
||||
phrase = ph.with_caveats(main, caveats, skip=("offset",) if code == "flat" else ())
|
||||
# 一份快照时 comparability 只会是本期核对不了的那句(同波动基线,只在证据面板);两份快照的可比性结论照旧进判定语
|
||||
skip = (("offset",) if code == "flat" else ()) + (("comparability",) if outcome is None else ())
|
||||
phrase = ph.with_caveats(main, caveats, skip=skip)
|
||||
return build(env, params, used, table=table, values=values, verdict=verdict,
|
||||
criterion=ph.criterion_additive(s, top_k_max=k_max, summed=summed), phrase=phrase, caveats=caveats)
|
||||
|
||||
@@ -487,6 +520,9 @@ def ratio(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
outcome, structure = _comparability(env, params, bs, ts, den)
|
||||
if structure is not None:
|
||||
return refuse(env, params, used, "incomparable_sides", f"{structure},不是同一个量的两期,不拆", criterion=criterion)
|
||||
partial, note = _period_complete(env, params, ts, den)
|
||||
if partial is not None:
|
||||
return refuse(env, params, used, "partial_target", partial, criterion=criterion)
|
||||
segs = _segments(side_b, side_t, (Fraction(0), Fraction(0)))
|
||||
live = [x for x in segs if x.base[1] != 0 or x.target[1] != 0]
|
||||
if len(live) < 2:
|
||||
@@ -504,7 +540,7 @@ def ratio(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
ph.say(ph.key_text(x.key), f"{when}的", den_label, "为 0 而", num_label,
|
||||
"不为 0,比率拆不了"), criterion=criterion)
|
||||
return _ratio_result(env, params, used, segs, by, dims, label, num_label, den_label, heads, parent_row, outcome,
|
||||
criterion, target_snap=ts)
|
||||
criterion, target_snap=ts, note=note)
|
||||
|
||||
|
||||
def ratio_effects(segs: Sequence[tuple[Fraction, Fraction, Fraction, Fraction]]
|
||||
@@ -533,7 +569,8 @@ def ratio_effects(segs: Sequence[tuple[Fraction, Fraction, Fraction, Fraction]]
|
||||
def _ratio_result(env: Env, params: dict[str, Any], used: list[str], segs: list[Segment], by: list[str],
|
||||
dims: list[str], label: str, num_label: str, den_label: str,
|
||||
heads: Mapping[str, tuple[Fraction, Any]], parent_row: Mapping[str, Any] | None,
|
||||
outcome: cmp.Outcome | None, criterion: str, *, target_snap: Snap) -> Any:
|
||||
outcome: cmp.Outcome | None, criterion: str, *, target_snap: Snap,
|
||||
note: Mapping[str, str] | None = None) -> Any:
|
||||
main_share, flat_pct = env.threshold("main_share"), env.threshold("flat_pct")
|
||||
min_den = env.threshold("min_denominator")
|
||||
live = [x for x in segs if x.base[1] != 0 or x.target[1] != 0]
|
||||
@@ -595,6 +632,8 @@ def _ratio_result(env: Env, params: dict[str, Any], used: list[str], segs: list[
|
||||
ph.say(num_label, "或", den_label))))
|
||||
if outcome is not None and outcome.verdict != "comparable":
|
||||
caveats.append(ph.caveat("comparability", outcome.phrase))
|
||||
if note is not None:
|
||||
caveats.append(dict(note))
|
||||
total_text = ph.ratio_change(R, R2, proportion=proportion)
|
||||
if code == "mix_driven":
|
||||
star = subjects[0]
|
||||
@@ -641,7 +680,8 @@ def _ratio_result(env: Env, params: dict[str, Any], used: list[str], segs: list[
|
||||
verdict = {"code": code, "subjects": [_key_strings(x.key) for x in subjects],
|
||||
"direction": sign_word(delta) if code in ("mix_driven", "rate_driven", "both") else None}
|
||||
return build(env, params, used, table=table, values=values, verdict=verdict, criterion=criterion,
|
||||
phrase=ph.with_caveats(main, caveats), caveats=caveats)
|
||||
phrase=ph.with_caveats(main, caveats, skip=("comparability",) if outcome is None else ()),
|
||||
caveats=caveats)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -715,6 +755,9 @@ def factor(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
outcome, structure = _comparability(env, params, bs, ts, product)
|
||||
if structure is not None:
|
||||
return refuse(env, params, used, "incomparable_sides", f"{structure},不是同一个量的两期,不拆", criterion=criterion)
|
||||
partial, note = _period_complete(env, params, ts, product)
|
||||
if partial is not None:
|
||||
return refuse(env, params, used, "partial_target", partial, criterion=criterion)
|
||||
if any(v <= 0 for row in cells.values() for v, _ in row):
|
||||
return refuse(env, params, used, "non_positive", "乘积或因子里有不大于 0 的值,对数拆解要求全为正", criterion=criterion)
|
||||
for when, row in cells.items():
|
||||
@@ -738,6 +781,8 @@ def factor(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
caveats: list[dict[str, str]] = []
|
||||
if outcome is not None and outcome.verdict != "comparable":
|
||||
caveats.append(ph.caveat("comparability", outcome.phrase))
|
||||
if note is not None:
|
||||
caveats.append(note)
|
||||
total_text = ph.change(delta, M)
|
||||
top = max(range(len(factors)), key=lambda i: shares[i] if shares[i] is not None else -math.inf)
|
||||
if flat:
|
||||
@@ -765,7 +810,8 @@ def factor(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
"top_share": canon(shares[top]) if shares[top] is not None else None}
|
||||
verdict = {"code": code, "subjects": subjects, "direction": sign_word(delta) if code != "flat" else None}
|
||||
return build(env, params, used, table=table, values=values, verdict=verdict, criterion=criterion,
|
||||
phrase=ph.with_caveats(main, caveats), caveats=caveats)
|
||||
phrase=ph.with_caveats(main, caveats, skip=("comparability",) if outcome is None else ()),
|
||||
caveats=caveats)
|
||||
|
||||
|
||||
__all__ = ["HEADLINE_SHAPE", "PARENT_SHAPE", "SAME_QUERY", "SIMPSON_SHARE", "USED", "additive", "factor", "lmdi",
|
||||
|
||||
@@ -271,6 +271,35 @@ def week_scheme(sql: str, column: str, dialect: str | None) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def plain_day(sql: str, column: str, dialect: str | None) -> bool:
|
||||
"""期次列是不是直接取的一个日期列(原样、date(列)、DATE(列)、CAST(列 AS DATE)),没有按周、按月换算过。"""
|
||||
from sqlglot import exp
|
||||
|
||||
from app.engine import cell_format as cf
|
||||
|
||||
try:
|
||||
value = _period_expr(sql, column, dialect)
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
if isinstance(value, (exp.Date, exp.TsOrDsToDate)):
|
||||
extra = [v for k, v in value.args.items() if k != "this" and v not in (None, [], False)]
|
||||
value = cf._unwrap(value.this) if not extra else None
|
||||
return isinstance(value, exp.Column) and not isinstance(value.this, exp.Star)
|
||||
|
||||
|
||||
def parse_compared(values: Sequence[Any], sql: str, column: str,
|
||||
dialect: str | None) -> tuple[list[Period], str] | None:
|
||||
"""按期次分组的一份快照里比的两期(变化拆解、可比性检查没给粒度时;快照里往往只有这两期):同 parse,周号按 SQL 认算法,
|
||||
粒度按值推断;但值都是日期、推断成按周、期次列又是直接取的日期列(plain_day)时按天认——两期正好隔 7 天,光看值分不出
|
||||
是两天还是两周,期次列没按周换算过就是那一天。"""
|
||||
parsed = parse(values, weeks=week_scheme(sql, column, dialect))
|
||||
if parsed is not None and parsed[1] == "week" and all(_shape(v) == "date" for v in values) \
|
||||
and plain_day(sql, column, dialect):
|
||||
return parse(values, "day")
|
||||
return parsed
|
||||
|
||||
|
||||
def _period_expr(sql: str, column: str, dialect: str | None) -> Any:
|
||||
"""结果列 column 的写法(sqlglot 节点):最外层 SELECT 里那一项;是从 CTE、子查询里原样取出的一列时往里找。"""
|
||||
from sqlglot import exp
|
||||
@@ -497,4 +526,4 @@ def weekend_days(start: date, end: date) -> int:
|
||||
|
||||
|
||||
__all__ = ["COUNT_WORD", "DEFAULT_LAG", "GRAINS", "PER_WORD", "WEEK_SCHEMES", "Period", "calendar_unit", "parse",
|
||||
"short_days", "unreadable", "week_scheme", "week_span", "weekend_days"]
|
||||
"parse_compared", "plain_day", "short_days", "unreadable", "week_scheme", "week_span", "weekend_days"]
|
||||
|
||||
@@ -57,8 +57,10 @@ async def test_a_driver_is_named_with_its_share_and_the_whole_shape_is_filled():
|
||||
"columns": ["region", "week", "gmv"], "rows": "all"}]
|
||||
assert result["hypothesis"] == "华东拖累" and result["probes"] == []
|
||||
assert result["criterion"].endswith("以各段之和为总数")
|
||||
assert {c["code"] for c in result["caveats"]} == {"headline_basis"}
|
||||
# 期次是现成的一列「2026-W41」,认不出是按哪种周号算的:本期齐不齐核对不了,只附在证据面板(不进判定语)
|
||||
assert {c["code"] for c in result["caveats"]} == {"headline_basis", "comparability"}
|
||||
assert result["params"]["catalog"] == {"labels": {"region": "地区"}, "measures": {"gmv": "flow"}}
|
||||
assert result["params"]["today"] == "2026-10-21"
|
||||
|
||||
|
||||
async def test_the_segments_add_up_to_the_total_digit_for_digit():
|
||||
@@ -163,7 +165,7 @@ async def test_drilling_down_checks_closure_against_the_parent_segment():
|
||||
assert child["params"]["depth"] == 1 and child["params"]["parent"] == {"of": "A3", "key": ["华东"]}
|
||||
assert child["inputs"][0]["role"] == "parent" and child["inputs"][0]["ref"] == "A3"
|
||||
assert "max_drill_depth" in child["settings"]
|
||||
assert {c["code"] for c in child["caveats"]} == set() # 有 parent 就不是「以各段之和为总数」
|
||||
assert {c["code"] for c in child["caveats"]} == {"comparability"} # 有 parent 就不是「以各段之和为总数」
|
||||
short = snap("Q4", province["sql"], ["province", "week", "gmv"],
|
||||
[["沪", "2026-W41", 500], ["沪", "2026-W42", 410], ["苏", "2026-W41", 300], ["苏", "2026-W42", 290]])
|
||||
unclosed = await analyze("decompose", "additive", snaps=[short], parents={"A3": top}, **params)
|
||||
@@ -504,3 +506,55 @@ async def test_a_dimension_named_like_a_result_column_is_a_call_error():
|
||||
error = await call_error("decompose", "ratio", snaps=[mixed], query="Q1", by="Mix", period="week", base="W41",
|
||||
target="W42", numerator="refunds", denominator="orders")
|
||||
assert error.code == "reserved_column" and "「Mix」" in error.message
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 一份快照按期次分组:本期齐不齐(同波动基线那一套)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
WEEK_START = ("SELECT region, date(order_date, '-6 days', 'weekday 1') AS week, SUM(amount) AS gmv FROM orders "
|
||||
"GROUP BY 1, 2")
|
||||
DAY_SQL = "SELECT region, order_date AS day, SUM(amount) AS gmv FROM orders GROUP BY 1, 2"
|
||||
|
||||
|
||||
def _two(base, target, *, sql=WEEK_START, col="week"):
|
||||
rows = [["华东", base, 820], ["华东", target, 700], ["华北", base, 600], ["华北", target, 560]]
|
||||
return snap("Q2", sql, ["region", col, "gmv"], rows, types={"region": "text", col: "text", "gmv": "number"})
|
||||
|
||||
|
||||
async def test_one_snapshot_by_period_checks_the_target_is_whole():
|
||||
probe = fixed_prober({("orders", "order_date"): ("2025-01-01", "2026-10-20")})
|
||||
ask = {"query": "Q2", "by": ["region"], "period": "week", "measure": "gmv", "label": "销售额"}
|
||||
# 运行当天 10-21:10-19 那一周没过完
|
||||
partial = await analyze("decompose", "additive", snaps=[_two("2026-10-12", "2026-10-19")], probe=probe,
|
||||
base="2026-10-12", target="2026-10-19", **ask)
|
||||
assert partial["reason_code"] == "partial_target"
|
||||
assert partial["reason"] == "本期数据只到 2026-10-20,还差 5 天,和完整的周比没有意义"
|
||||
assert partial["params"]["today"] == "2026-10-21" and partial["probes"][0]["column"] == "order_date"
|
||||
whole = await analyze("decompose", "additive", snaps=[_two("2026-10-05", "2026-10-12")], probe=probe,
|
||||
base="2026-10-05", target="2026-10-12", **ask)
|
||||
assert whole["status"] == "ok" and not [c for c in whole["caveats"] if c["code"] == "comparability"]
|
||||
# 查不到表的范围:只附一句核对不了,不进判定语
|
||||
unknown = await analyze("decompose", "additive", snaps=[_two("2026-10-05", "2026-10-12")],
|
||||
base="2026-10-05", target="2026-10-12", **ask)
|
||||
note = next(c["text"] for c in unknown["caveats"] if c["code"] == "comparability")
|
||||
assert note == "没能核对本期的数据是否到齐(查不到表里这一列的日期范围)" and note not in unknown["phrase"]
|
||||
ratio_rows = [["线上", "2026-10-12", 50, 1000], ["线上", "2026-10-19", 80, 1000], ["线下", "2026-10-12", 20, 1000],
|
||||
["线下", "2026-10-19", 26, 1000]]
|
||||
ratio_snap = snap("Q1", "SELECT channel, date(order_date, '-6 days', 'weekday 1') AS week, SUM(refund_flag) AS "
|
||||
"refunds, COUNT(*) AS orders FROM orders GROUP BY 1, 2",
|
||||
["channel", "week", "refunds", "orders"], ratio_rows)
|
||||
refused = await analyze("decompose", "ratio", snaps=[ratio_snap], probe=probe, query="Q1", by="channel",
|
||||
period="week", base="2026-10-12", target="2026-10-19", numerator="refunds",
|
||||
denominator="orders")
|
||||
assert refused["reason_code"] == "partial_target"
|
||||
|
||||
|
||||
async def test_two_days_a_week_apart_from_a_plain_date_column_are_two_days():
|
||||
# 期次列直接取的日期:昨天(10-20)对上周同一天,不是两周;10-20 当天的数据已经到了
|
||||
probe = fixed_prober({("orders", "order_date"): ("2025-01-01", "2026-10-20")})
|
||||
result = await analyze("decompose", "additive", snaps=[_two("2026-10-13", "2026-10-20", sql=DAY_SQL, col="day")],
|
||||
probe=probe, query="Q2", by=["region"], period="day", base="2026-10-13",
|
||||
target="2026-10-20", measure="gmv")
|
||||
assert result["status"] == "ok" and not [c for c in result["caveats"] if c["code"] == "comparability"]
|
||||
|
||||
@@ -136,7 +136,7 @@ async def test_the_phrase_opens_judgment_method_calculation_inputs_recompute_and
|
||||
assert judged["cited"] == {"what": "phrase"} and judged["render_ok"] is True and judged["eid_ok"] is True
|
||||
assert judged["origin"] == "card" and judged["item"] == "drop" and judged["label"] == "分析"
|
||||
assert judged["hypothesis"] == "华东拖累" and judged["hypothesis_note"].startswith("Agent 写的假设")
|
||||
assert {c["code"] for c in judged["caveats"]} == {"measure_unlabeled", "headline_basis"}
|
||||
assert {c["code"] for c in judged["caveats"]} == {"measure_unlabeled", "headline_basis", "comparability"}
|
||||
assert method["method"] == "decompose.additive@1" and method["method_label"] == "变化拆解(可加)"
|
||||
assert {s["key"]: s["label"] for s in method["settings"]}["main_share"] == "「主要来源」的占比"
|
||||
assert {s["key"] for s in method["settings"]} >= {"main_share", "concentration", "table_top_k"}
|
||||
|
||||
@@ -128,3 +128,15 @@ def test_unreadable_values_are_named():
|
||||
assert pd.unreadable(["2026-W40", "上周", "2026-W41", None]) == ("shape", ["上周", None])
|
||||
assert pd.unreadable(["2026-W40", "2026-W41", "2026-10-12"]) == ("mixed", ["2026-10-12"])
|
||||
assert pd.unreadable(["2026-10", "2026-11"], "week") == ("grain", ["2026-10", "2026-11"])
|
||||
|
||||
|
||||
def test_a_plain_date_column_is_a_day_even_when_two_values_are_a_week_apart():
|
||||
plain = "SELECT region, order_date AS day, SUM(amount) AS gmv FROM orders GROUP BY 1, 2"
|
||||
starts = ("SELECT region, date(order_date, '-6 days', 'weekday 1') AS week, SUM(amount) AS gmv FROM orders "
|
||||
"GROUP BY 1, 2")
|
||||
assert pd.plain_day(plain, "day", "sqlite") and pd.plain_day(plain.replace("order_date AS", "DATE(order_date) AS"),
|
||||
"day", "sqlite")
|
||||
assert not pd.plain_day(starts, "week", "sqlite")
|
||||
assert pd.parse_compared(["2026-10-13", "2026-10-20"], plain, "day", "sqlite")[1] == "day"
|
||||
assert pd.parse_compared(["2026-10-12", "2026-10-19"], starts, "week", "sqlite")[1] == "week"
|
||||
assert pd.parse_compared(["2026-09-01", "2026-10-01"], plain, "day", "sqlite")[1] == "month"
|
||||
|
||||
Reference in New Issue
Block a user