跳到正文
cccskills
免费在 GitHub 公开

dataframe-performance

Use when dataframe code is slow or runs out of memory, when data outgrows pandas, or when writing pandas 3, Polars or DuckDB code — vectorisation, dtypes, Copy-on-Write, lazy and out-of-core execution, and measuring before and after

中文简介正在准备中。当前显示原文说明。

查看安装方法

包含的文件(1)

  • SKILL.md6.4 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

DataFrame Performance

Overview

Most slow data code is Python looping over rows, reading columns it never uses, or holding strings as generic objects. Fix those in the library the repository already uses before reaching for a new one; when the data truly outgrows memory, DuckDB and Polars process it lazily and out of core.

Core principle: measure first, pin behaviour with a test, optimise, measure again — and report both numbers.

1. Measure, then pin

start = time.perf_counter()
result = build_features(sample)
print(f"{time.perf_counter() - start:.2f}s, {result.memory_usage(deep=True).sum() / 1e6:.0f} MB")

Measure on a realistic sample (not 20 rows, not the full warehouse). Before rewriting a slow function, make sure a fixture test pins its output; the faster version must pass the same test. Put "before → after" timings and memory in your closing message.

2. pandas 3 semantics you must write for

pandas 3 makes Copy-on-Write the only mode and a dedicated string dtype the default. Code written for pandas 1.x/2.x can silently stop working:

# ❌ chained assignment — never updates df under Copy-on-Write
df["discount"][df["plan"] == "pro"] = 0.1
# ❌ inplace on a selected column — never updates df
df["age"].fillna(0, inplace=True)

# ✅ one indexing operation on the object you mean to change
df.loc[df["plan"] == "pro", "discount"] = 0.1
df["age"] = df["age"].fillna(0)
  • A filtered frame behaves as a copy: sub = df[mask]; sub["x"] = 1 changes sub only, with no SettingWithCopyWarning and no .copy() needed.
  • Strings default to the str dtype (Arrow-backed when PyArrow is installed; missing values are NaN). df["s"].dtype == object is now False — use pd.api.types.is_string_dtype.
  • The repository's pinned pandas wins; if it is still on 2.x, the patterns above are also the correct 2.x style.

3. Vectorise instead of looping

# ❌ Python function per row
df["tier"] = df.apply(lambda r: "high" if r.spend > 1000 else ("mid" if r.spend > 100 else "low"), axis=1)

# ✅ vectorised
df["tier"] = np.select([df["spend"] > 1000, df["spend"] > 100], ["high", "mid"], default="low")
# ✅ or as an ordered categorical
df["tier"] = pd.cut(df["spend"], bins=[-np.inf, 100, 1000, np.inf], labels=["low", "mid", "high"])
Instead ofUse
iterrows, itertuples, apply(axis=1)column arithmetic, np.where, np.select, pd.cut
a loop doing dict lookupsseries.map(mapping) or a merge
groupby, then merge the result backgroupby(...)[col].transform("sum")
Python string functions per valuethe .str accessor (fast on the Arrow-backed dtype)
pd.concat inside a loopcollect frames in a list, concat once

4. Read less, store smaller

pd.read_parquet("events.parquet", columns=["user_id", "ts", "amount"], filters=[("ts", ">=", start)])
pd.read_csv(path, usecols=["user_id", "plan"], dtype={"plan": "category"})
  • Parquet over CSV for anything read more than once; select columns and push filters into the read.
  • category for low-cardinality strings; float32 for model features where the precision is enough.
  • Check df.memory_usage(deep=True) before and after.
  • Drop intermediate frames you no longer need inside long functions; do not keep five versions of the same table alive.

5. DuckDB: SQL over files, out of core

con = duckdb.connect()
con.execute("SET memory_limit = '4GB'")
daily = con.execute(
    """
    select user_id, date_trunc('day', ts) as day, sum(amount) as spend
    from read_parquet('data/events/*/*.parquet', hive_partitioning = true)
    where ts >= ?
    group by all
    """,
    [start],
).df()
  • Reads Parquet/CSV directly, spills to disk past the memory limit, and can query an in-scope pandas or Polars frame by its variable name.
  • .df() returns pandas, .pl() Polars; COPY (select ...) TO 'out.parquet' (FORMAT parquet) writes without a round-trip through Python.
  • Bind values with ? parameters; EXPLAIN ANALYZE shows where time goes.

6. Polars: lazy expressions

spend = (
    pl.scan_parquet("data/events/*.parquet")
    .filter(pl.col("ts") >= start)
    .group_by("user_id")
    .agg(pl.col("amount").sum().alias("spend"), pl.len().alias("n_events"))
    .collect(engine="streaming")
)
  • scan_* + collect() lets the optimiser push filters and column selection into the read; .explain() prints the plan.
  • pl.when(...).then(...).otherwise(...) and .over("user_id") replace row loops and groupby-merge-back.
  • map_elements runs Python per value — the same trap as apply(axis=1).
  • Polars 2.0 shipped in October 2026 with breaking changes. A repository pinned to polars<2 stays there; the code above is written against the 1.x API — check the changelog before using it on 2.x.

7. Choosing — the repository decides first

SituationDo
Repo uses pandas, data fits in memoryoptimise the pandas code (sections 2–4)
Repo already uses DuckDB or Polarsuse it for the heavy step
Data outgrows memory and the repo has neithersay so in a comment and propose DuckDB or Polars; do not add a second dataframe library in a feature task (rule stack-choice)
Heavy aggregation already in the warehousepush it into SQL or dbt instead of pulling raw rows

Convert between libraries once, at a boundary — not back and forth inside a loop.

Common Mistakes

  • Optimising without a before number, so nobody knows whether it helped.
  • apply(axis=1) "because it is readable".
  • Reading a whole wide Parquet file to use three columns.
  • Chained assignment carried over from pandas 1.x code into a pandas 3 repository.
  • Converting pandas ↔ Polars per batch inside a loop.

Red Flags

  • A function whose runtime grows faster than its input size.
  • Memory several times the on-disk Parquet size.
  • object dtype columns full of numbers.
  • A ChainedAssignmentError warning in the test output.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

中文简介正在准备中。当前显示原文说明。

makifbaysal/tasktrooper1172026年10月18日 更新

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

中文简介正在准备中。当前显示原文说明。

makifbaysal/tasktrooper1172026年10月18日 更新

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

中文简介正在准备中。当前显示原文说明。

makifbaysal/tasktrooper1172026年10月18日 更新

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

中文简介正在准备中。当前显示原文说明。

makifbaysal/tasktrooper1172026年10月18日 更新

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

中文简介正在准备中。当前显示原文说明。

makifbaysal/tasktrooper1172026年10月18日 更新

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

中文简介正在准备中。当前显示原文说明。

makifbaysal/tasktrooper1172026年10月18日 更新

查看 makifbaysal 的全部技能

报告此技能的问题