@zigrivers/scaffold 3.22.0 → 3.23.0
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.
- package/README.md +21 -7
- package/content/knowledge/data-science/README.md +23 -0
- package/content/knowledge/data-science/data-science-architecture.md +163 -0
- package/content/knowledge/data-science/data-science-conventions.md +233 -0
- package/content/knowledge/data-science/data-science-data-versioning.md +198 -0
- package/content/knowledge/data-science/data-science-dev-environment.md +159 -0
- package/content/knowledge/data-science/data-science-experiment-tracking.md +194 -0
- package/content/knowledge/data-science/data-science-model-evaluation.md +160 -0
- package/content/knowledge/data-science/data-science-notebook-discipline.md +170 -0
- package/content/knowledge/data-science/data-science-observability.md +161 -0
- package/content/knowledge/data-science/data-science-project-structure.md +178 -0
- package/content/knowledge/data-science/data-science-reproducibility.md +164 -0
- package/content/knowledge/data-science/data-science-requirements.md +151 -0
- package/content/knowledge/data-science/data-science-security.md +151 -0
- package/content/knowledge/data-science/data-science-testing.md +183 -0
- package/content/knowledge/ml/README.md +10 -0
- package/content/methodology/data-science-overlay.yml +39 -0
- package/dist/config/schema.d.ts +672 -126
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +8 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +2 -2
- package/dist/config/schema.test.js.map +1 -1
- package/dist/config/validators/data-science.d.ts +4 -0
- package/dist/config/validators/data-science.d.ts.map +1 -0
- package/dist/config/validators/data-science.js +15 -0
- package/dist/config/validators/data-science.js.map +1 -0
- package/dist/config/validators/index.d.ts.map +1 -1
- package/dist/config/validators/index.js +2 -0
- package/dist/config/validators/index.js.map +1 -1
- package/dist/core/assembly/knowledge-loader.d.ts.map +1 -1
- package/dist/core/assembly/knowledge-loader.js +6 -0
- package/dist/core/assembly/knowledge-loader.js.map +1 -1
- package/dist/core/assembly/knowledge-loader.test.js +34 -0
- package/dist/core/assembly/knowledge-loader.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.js +73 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/project/adopt.d.ts.map +1 -1
- package/dist/project/adopt.js +3 -1
- package/dist/project/adopt.js.map +1 -1
- package/dist/project/detectors/coverage.test.d.ts +2 -0
- package/dist/project/detectors/coverage.test.d.ts.map +1 -0
- package/dist/project/detectors/coverage.test.js +78 -0
- package/dist/project/detectors/coverage.test.js.map +1 -0
- package/dist/project/detectors/data-science.d.ts +4 -0
- package/dist/project/detectors/data-science.d.ts.map +1 -0
- package/dist/project/detectors/data-science.js +32 -0
- package/dist/project/detectors/data-science.js.map +1 -0
- package/dist/project/detectors/data-science.test.d.ts +2 -0
- package/dist/project/detectors/data-science.test.d.ts.map +1 -0
- package/dist/project/detectors/data-science.test.js +62 -0
- package/dist/project/detectors/data-science.test.js.map +1 -0
- package/dist/project/detectors/disambiguate.d.ts +2 -0
- package/dist/project/detectors/disambiguate.d.ts.map +1 -1
- package/dist/project/detectors/disambiguate.js +3 -2
- package/dist/project/detectors/disambiguate.js.map +1 -1
- package/dist/project/detectors/disambiguate.test.js +10 -1
- package/dist/project/detectors/disambiguate.test.js.map +1 -1
- package/dist/project/detectors/index.d.ts.map +1 -1
- package/dist/project/detectors/index.js +2 -0
- package/dist/project/detectors/index.js.map +1 -1
- package/dist/project/detectors/library.d.ts.map +1 -1
- package/dist/project/detectors/library.js +1 -0
- package/dist/project/detectors/library.js.map +1 -1
- package/dist/project/detectors/resolve-detection.test.js +31 -0
- package/dist/project/detectors/resolve-detection.test.js.map +1 -1
- package/dist/project/detectors/types.d.ts +6 -2
- package/dist/project/detectors/types.d.ts.map +1 -1
- package/dist/project/detectors/types.js.map +1 -1
- package/dist/types/config.d.ts +8 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/wizard/copy/core.d.ts.map +1 -1
- package/dist/wizard/copy/core.js +4 -0
- package/dist/wizard/copy/core.js.map +1 -1
- package/dist/wizard/copy/data-science.d.ts +3 -0
- package/dist/wizard/copy/data-science.d.ts.map +1 -0
- package/dist/wizard/copy/data-science.js +15 -0
- package/dist/wizard/copy/data-science.js.map +1 -0
- package/dist/wizard/copy/index.d.ts.map +1 -1
- package/dist/wizard/copy/index.js +2 -0
- package/dist/wizard/copy/index.js.map +1 -1
- package/dist/wizard/copy/types.d.ts +5 -1
- package/dist/wizard/copy/types.d.ts.map +1 -1
- package/dist/wizard/copy/types.test-d.js +7 -0
- package/dist/wizard/copy/types.test-d.js.map +1 -1
- package/dist/wizard/questions.d.ts +2 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +9 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +14 -0
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +1 -0
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Either way, Scaffold constructs the prompt and the target AI tool does the work.
|
|
|
29
29
|
|
|
30
30
|
**Assembly engine** — At execution time, Scaffold builds a 7-section prompt from: system metadata, the meta-prompt, knowledge base entries, project context (artifacts from prior steps), methodology settings, layered instructions, and depth-specific execution guidance.
|
|
31
31
|
|
|
32
|
-
**Knowledge base** —
|
|
32
|
+
**Knowledge base** — 235 domain expertise entries in `content/knowledge/` organized in eighteen categories (core, product, review, validation, finalization, execution, tools, game, web-app, backend, cli, library, mobile-app, data-pipeline, ml, browser-extension, research, data-science) covering testing strategy, domain modeling, API design, security best practices, eval craft, TDD execution, task claiming, worktree management, release management, rendering strategies, data stores, CLI patterns, game engines, library bundling, mobile deployment, batch and streaming pipelines, model training and serving, browser extension manifests and service workers, data-science reproducibility and notebook discipline, and more. These get injected into prompts based on each step's `knowledge-base` frontmatter field. Knowledge files with a `## Deep Guidance` section are optimized for CLI assembly — only the deep guidance content is loaded, avoiding redundancy with the prompt text. Teams can add project-local overrides in `.scaffold/knowledge/` that layer on top of the global entries.
|
|
33
33
|
|
|
34
34
|
**Methodology presets** — Three built-in presets control which steps run and how deep the analysis goes:
|
|
35
35
|
- **deep** (depth 5) — all steps enabled, exhaustive analysis
|
|
@@ -368,7 +368,7 @@ Every `scaffold init` wizard question can be answered via CLI flags, making scaf
|
|
|
368
368
|
| `--depth` | 1-5 | Custom methodology depth (requires `--methodology custom`) |
|
|
369
369
|
| `--adapters` | comma-sep | AI adapters: claude-code, codex, gemini |
|
|
370
370
|
| `--traits` | comma-sep | Project traits: web, mobile |
|
|
371
|
-
| `--project-type` | string | web-app, mobile-app, backend, cli, library, game, data-pipeline, ml, browser-extension, research |
|
|
371
|
+
| `--project-type` | string | web-app, mobile-app, backend, cli, library, game, data-pipeline, ml, browser-extension, research, data-science |
|
|
372
372
|
| `--auto` | boolean | Non-interactive mode (uses Zod defaults for unset flags) |
|
|
373
373
|
|
|
374
374
|
#### Web-App Config Flags (require `--project-type web-app` or auto-set it)
|
|
@@ -457,6 +457,14 @@ Every `scaffold init` wizard question can be answered via CLI flags, making scaf
|
|
|
457
457
|
| `--research-domain` | string | none, quant-finance, ml-research, simulation |
|
|
458
458
|
| `--research-tracking` | boolean | `--research-tracking` / `--no-research-tracking` |
|
|
459
459
|
|
|
460
|
+
#### Data Science Config (`--project-type data-science`)
|
|
461
|
+
|
|
462
|
+
Data science has one forward-compatible config field in the schema, defaulted automatically — no CLI flags are needed in v1:
|
|
463
|
+
|
|
464
|
+
| Config field | Values | Notes |
|
|
465
|
+
|------|------|--------|
|
|
466
|
+
| `dataScienceConfig.audience` | `solo` | Default (applied by the wizard and `--auto`). Covers the DS-1 audience (solo / small-team, local-first, prototyping). A future DS-2 release will extend the enum with `'platform'` (platform-engineered / larger-team DS) additively, without breaking existing configs. |
|
|
467
|
+
|
|
460
468
|
#### Game Config Flags (require `--project-type game` or auto-set it)
|
|
461
469
|
|
|
462
470
|
| Flag | Type | Values |
|
|
@@ -506,7 +514,7 @@ during assembly.
|
|
|
506
514
|
|
|
507
515
|
- **Flag > auto > interactive**: Flags always take highest precedence. `--auto --engine unreal` uses defaults for everything except engine.
|
|
508
516
|
- **Partial flags + interactive**: Provide some flags and the wizard asks only the remaining questions. `scaffold init --project-type game --engine unreal` prompts interactively for multiplayer, platforms, etc.
|
|
509
|
-
- **Type-specific flags auto-set project type**: `--engine unity` automatically sets `--project-type game`, `--web-rendering ssr` sets `--project-type web-app`, `--backend-api-style rest` sets `--project-type backend`, `--cli-interactivity hybrid` sets `--project-type cli`, `--lib-visibility public` sets `--project-type library`, `--mobile-platform ios` sets `--project-type mobile-app`, `--pipeline-processing batch` sets `--project-type data-pipeline`, `--ml-phase training` sets `--project-type ml`, `--ext-manifest 3` sets `--project-type browser-extension`, `--research-driver code-driven` sets `--project-type research`. Error if conflicting type.
|
|
517
|
+
- **Type-specific flags auto-set project type**: `--engine unity` automatically sets `--project-type game`, `--web-rendering ssr` sets `--project-type web-app`, `--backend-api-style rest` sets `--project-type backend`, `--cli-interactivity hybrid` sets `--project-type cli`, `--lib-visibility public` sets `--project-type library`, `--mobile-platform ios` sets `--project-type mobile-app`, `--pipeline-processing batch` sets `--project-type data-pipeline`, `--ml-phase training` sets `--project-type ml`, `--ext-manifest 3` sets `--project-type browser-extension`, `--research-driver code-driven` sets `--project-type research`. Error if conflicting type. (Data science currently has no dedicated CLI flags — pass `--project-type data-science` directly.)
|
|
510
518
|
- **Cannot mix flag families**: `--web-rendering ssr --backend-api-style rest` is an error. Each flag family (`--web-*`, `--backend-*`, `--cli-*`, `--lib-*`, `--mobile-*`, `--pipeline-*`, `--ml-*`, `--research-*`, `--ext-*`, game) is exclusive.
|
|
511
519
|
- **Validation**: `--depth` requires `--methodology custom`. `--online-services` requires `--multiplayer online` or `hybrid`. SSR/hybrid rendering is incompatible with static deploy target. Session auth requires server state (not static). ML inference projects must specify a serving pattern. Browser extensions must declare at least one capability (UI surface, content script, or background worker). Notebook-driven research cannot be fully autonomous.
|
|
512
520
|
|
|
@@ -599,6 +607,9 @@ scaffold init --auto --methodology deep --project-type research \
|
|
|
599
607
|
--research-driver config-driven --research-interaction checkpoint-gated \
|
|
600
608
|
--research-domain ml-research
|
|
601
609
|
|
|
610
|
+
# Solo / small-team data science project (reproducibility-first)
|
|
611
|
+
scaffold init --auto --methodology deep --project-type data-science
|
|
612
|
+
|
|
602
613
|
# Multiplayer mobile game with Unity
|
|
603
614
|
scaffold init --project-type game --methodology deep --auto \
|
|
604
615
|
--engine unity --multiplayer online --target-platforms ios,android \
|
|
@@ -625,7 +636,7 @@ Scaffold supports **project-type overlays** — domain-specific knowledge and pi
|
|
|
625
636
|
|
|
626
637
|
- **Injects domain knowledge** into existing pipeline steps (e.g., SSR caching strategies into `tech-stack`, API pagination patterns into `coding-standards`)
|
|
627
638
|
|
|
628
|
-
The game overlay additionally adjusts step enablement, remaps artifact references, and adds dependency overrides (because game development has fundamentally different artifacts). The web-app, backend, CLI, library, mobile-app, data-pipeline, ML, browser-extension, and
|
|
639
|
+
The game overlay additionally adjusts step enablement, remaps artifact references, and adds dependency overrides (because game development has fundamentally different artifacts). The web-app, backend, CLI, library, mobile-app, data-pipeline, ML, browser-extension, research, and data-science overlays are **knowledge-only** — they inject domain expertise into existing steps without changing which steps run or how they depend on each other. The research type additionally supports **domain sub-overlays** (quant-finance, ml-research, simulation) that layer domain-specific knowledge on top of the core research overlay, and the backend type supports a `fintech` sub-overlay. Both research and backend accept `domain` as either a single string or an array (e.g. `domain: ['quant-finance', 'simulation']`) for stacking multiple sub-overlays; the wizard and CLI flags remain single-select in v1, so multi-domain stacking requires hand-editing `.scaffold/config.yml`.
|
|
629
640
|
|
|
630
641
|
Overlays are composable with methodology presets. An MVP web-app gets fewer steps at lower depth; a deep backend project gets exhaustive analysis of every architectural decision.
|
|
631
642
|
|
|
@@ -640,6 +651,7 @@ Overlays are composable with methodology presets. An MVP web-app gets fewer step
|
|
|
640
651
|
| `ml` | `ml-overlay.yml` | 12 entries (architecture, training and serving patterns, experiment tracking, model evaluation, observability, testing, security) | Project phase, model type, serving pattern, experiment tracking |
|
|
641
652
|
| `browser-extension` | `browser-extension-overlay.yml` | 12 entries (architecture, manifest configuration, service workers, content scripts, cross-browser, store submission, testing, security) | Manifest version, UI surfaces, content script, background worker |
|
|
642
653
|
| `research` | `research-overlay.yml` + domain sub-overlays | 25 entries (experiment loop, tracking, overfitting prevention, backtesting, risk metrics, architecture search, simulation) | Experiment driver, interaction mode, domain, experiment tracking |
|
|
654
|
+
| `data-science` | `data-science-overlay.yml` | 13 entries (reproducibility, experiment tracking, notebook discipline, model evaluation, data versioning, dev environment, observability, project structure, conventions, requirements, security, testing, architecture) | Audience (`solo` default; `platform` reserved for DS-2) |
|
|
643
655
|
| `game` | `game-overlay.yml` | 24 entries (engines, networking, audio, VR/AR, economy, save systems, certification) | Engine, multiplayer, platforms, economy, narrative, and 6 more |
|
|
644
656
|
|
|
645
657
|
### Game Development
|
|
@@ -725,7 +737,7 @@ These answers control which conditional steps activate. A single-player puzzle g
|
|
|
725
737
|
|
|
726
738
|
#### Multi-type Detection
|
|
727
739
|
|
|
728
|
-
`scaffold adopt` detects
|
|
740
|
+
`scaffold adopt` detects 11 project types from manifest files and directory layouts:
|
|
729
741
|
|
|
730
742
|
| Type | Key Signals |
|
|
731
743
|
|------|-------------|
|
|
@@ -739,6 +751,7 @@ These answers control which conditional steps activate. A single-player puzzle g
|
|
|
739
751
|
| `ml` | `training/`/`models/` dirs, PyTorch/TensorFlow deps, MLflow configs |
|
|
740
752
|
| `browser-extension` | `manifest.json` with `manifest_version` field |
|
|
741
753
|
| `research` | `program.md` + `results.tsv`, backtest/strategy files with trading deps, optimization deps + experiment dirs, simulation framework deps |
|
|
754
|
+
| `data-science` | Marimo signals required (`marimo` dep or `.marimo.toml`); DVC (`dvc.yaml`, `.dvc/config`, `dvc` py dep) is supplementary evidence only. Low-tier; defers to `ml` / `research` / `data-pipeline` when those match at medium/high tier |
|
|
742
755
|
|
|
743
756
|
Each detector returns a confidence tier (high/medium/low) with evidence trails. Override detection with `--project-type <type>`.
|
|
744
757
|
|
|
@@ -1374,7 +1387,7 @@ scaffold dashboard
|
|
|
1374
1387
|
|
|
1375
1388
|
## Knowledge System
|
|
1376
1389
|
|
|
1377
|
-
Scaffold ships with
|
|
1390
|
+
Scaffold ships with 235 domain expertise entries organized in eighteen categories:
|
|
1378
1391
|
|
|
1379
1392
|
- **core/** (26 entries) — eval craft, testing strategy, domain modeling, API design, database design, system architecture, ADR craft, security best practices, operations, task decomposition, user stories, UX specification, design system tokens, user story innovation, AI memory management, coding conventions, tech stack selection, project structure patterns, task tracking, CLAUDE.md patterns, multi-model review dispatch, review step template, dev environment, git workflow patterns, automated review tooling, vision craft
|
|
1380
1393
|
- **product/** (5 entries) — PRD craft, PRD innovation, gap analysis, vision craft, vision innovation
|
|
@@ -1393,6 +1406,7 @@ Scaffold ships with 222 domain expertise entries organized in sixteen categories
|
|
|
1393
1406
|
- **ml/** (12 entries) — training and inference patterns, model types (classical/deep-learning/llm), serving patterns, experiment tracking, model evaluation, MLOps observability
|
|
1394
1407
|
- **browser-extension/** (12 entries) — Manifest V3, content scripts, service workers, cross-browser compatibility, extension security, store submission
|
|
1395
1408
|
- **research/** (25 entries) — experiment loop architecture, parameter optimization, overfitting prevention, experiment tracking, security/sandboxing; domain knowledge for quant-finance (backtesting, risk metrics, market data, strategy patterns), ML-research (architecture search, ablation studies, evaluation), and simulation (engine integration, parameter spaces, compute management)
|
|
1409
|
+
- **data-science/** (13 entries) — reproducibility, experiment tracking, notebook discipline, model evaluation, data versioning, dev environment (Marimo/Jupyter/Hex), observability, project structure, conventions, requirements, security, testing, architecture
|
|
1396
1410
|
|
|
1397
1411
|
Each pipeline step declares which knowledge entries it needs in its frontmatter. The assembly engine injects them automatically. Knowledge files with a `## Deep Guidance` section are optimized for the CLI — only the deep guidance content is loaded into the assembled prompt, skipping the summary to avoid redundancy with the prompt text.
|
|
1398
1412
|
|
|
@@ -1599,7 +1613,7 @@ All build inputs live under `content/`:
|
|
|
1599
1613
|
content/
|
|
1600
1614
|
├── pipeline/ # 60 meta-prompts organized by 16 phases (phases 0-15, including build)
|
|
1601
1615
|
├── tools/ # 10 tool meta-prompts (stateless, category: tool)
|
|
1602
|
-
├── knowledge/ #
|
|
1616
|
+
├── knowledge/ # 235 domain expertise entries (core, product, review, validation, finalization, execution, tools, game, web-app, backend, cli, library, mobile-app, data-pipeline, ml, browser-extension, research, data-science)
|
|
1603
1617
|
├── methodology/ # 3 YAML presets (deep, mvp, custom)
|
|
1604
1618
|
└── skills/ # Skill templates with {{markers}} for multi-platform resolution (includes mmr)
|
|
1605
1619
|
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# `data-science/` knowledge
|
|
2
|
+
|
|
3
|
+
Solo / small-team data-science domain knowledge injected into universal pipeline
|
|
4
|
+
steps by `content/methodology/data-science-overlay.yml`.
|
|
5
|
+
|
|
6
|
+
## Lockstep pairs with `ml/`
|
|
7
|
+
|
|
8
|
+
Five documents here mirror documents in `content/knowledge/ml/`. The two
|
|
9
|
+
overlays never compose at runtime (a user picks exactly one project type), but
|
|
10
|
+
edits to one side of a pair should trigger review of the other to prevent
|
|
11
|
+
recommendation drift over time:
|
|
12
|
+
|
|
13
|
+
| `data-science/` | `ml/` |
|
|
14
|
+
| --------------------------------------- | -------------------------------- |
|
|
15
|
+
| `data-science-experiment-tracking.md` | `ml-experiment-tracking.md` |
|
|
16
|
+
| `data-science-model-evaluation.md` | `ml-model-evaluation.md` |
|
|
17
|
+
| `data-science-observability.md` | `ml-observability.md` |
|
|
18
|
+
| `data-science-requirements.md` | `ml-requirements.md` |
|
|
19
|
+
| `data-science-conventions.md` | `ml-conventions.md` |
|
|
20
|
+
|
|
21
|
+
`ml/` targets production training and serving systems. `data-science/` targets
|
|
22
|
+
solo / small-team analytics and prototyping. Tool picks may diverge where the
|
|
23
|
+
audience justifies it (e.g. MLflow self-hosted vs managed W&B).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-science-architecture
|
|
3
|
+
description: Local-first architecture for solo and small-team data science — notebook exploration, src/ promotion, idempotent entrypoint pipelines, Polars vs Pandas choice, and artifact separation
|
|
4
|
+
topics: [data-science, architecture, polars, pandas, notebook-promotion]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
"Architecture" sounds heavy for a single analyst opening a notebook, but it is the one decision that separates work a collaborator can rerun tomorrow from a pile of ad-hoc scripts that only you can coax back to life. Solo DS work is local-first, reproducibility-first, and almost never needs Airflow or a Kubernetes cluster. What it needs is a coherent shape that scales from "a single notebook" to "a pipeline a teammate can clone and run" — and a clear story about where raw data, intermediate data, models, and reports each live. This doc lays out that shape and the small set of conventions that make it hold together.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Architect a solo DS project as layers: exploratory notebooks on top, reusable functions in `src/`, unit tests in `tests/`, and a thin entrypoint script that composes those functions into a reproducible run. Use Polars for datasets >1 GB or >10M rows and Pandas for everything smaller where scikit-learn / seaborn compatibility matters. Runs happen via `uv run python -m src.pipeline` — no scheduler needed. Pipelines are idempotent functions that move data from `data/raw/` to `data/interim/` to `data/processed/`, emitting models to `models/` and reports to `reports/`. This shape deliberately does not solve distributed data, production serving, or real-time inference — when those become real, graduate to Prefect / Dagster and cross over to `ml-serving-patterns.md`.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### The layered shape
|
|
16
|
+
|
|
17
|
+
The entire architecture is five layers, each with a single responsibility:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
21
|
+
│ notebooks/ exploration, narrative, charts │
|
|
22
|
+
│ ↓ (promote stable code) │
|
|
23
|
+
├──────────────────────────────────────────────────────────────┤
|
|
24
|
+
│ src/<project>/ typed, importable functions │
|
|
25
|
+
│ ↓ (test every function you ship) │
|
|
26
|
+
├──────────────────────────────────────────────────────────────┤
|
|
27
|
+
│ tests/ pytest smoke + unit tests │
|
|
28
|
+
│ ↓ (functions compose into a run) │
|
|
29
|
+
├──────────────────────────────────────────────────────────────┤
|
|
30
|
+
│ src/pipeline.py entrypoint: load→features→train→save │
|
|
31
|
+
│ ↓ (run produces artifacts) │
|
|
32
|
+
├──────────────────────────────────────────────────────────────┤
|
|
33
|
+
│ data/ models/ reports/ outputs, gitignored or DVC-tracked │
|
|
34
|
+
└──────────────────────────────────────────────────────────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Read top-to-bottom it is the promotion path; read bottom-to-top it is the dependency graph. A notebook may import from `src/` but `src/` must never import from a notebook. Tests depend only on `src/`. The entrypoint (`pipeline.py`) is itself a module under `src/`, not a loose script at the repo root — keeping it importable lets you exercise it end-to-end in tests with a tiny fixture dataset.
|
|
38
|
+
|
|
39
|
+
### Polars vs Pandas
|
|
40
|
+
|
|
41
|
+
Pick the DataFrame library based on data size and ecosystem needs, not on what's trendy. Rule of thumb:
|
|
42
|
+
|
|
43
|
+
| Dimension | Pandas | Polars |
|
|
44
|
+
|----------------------|---------------------------------------|-----------------------------------------|
|
|
45
|
+
| Rows | <10M comfortably | 10M–1B on a single machine |
|
|
46
|
+
| In-memory size | <1 GB | 1 GB – ~RAM/2 |
|
|
47
|
+
| Execution | Eager, single-threaded | Lazy + multi-threaded, Arrow-native |
|
|
48
|
+
| Ecosystem | scikit-learn, seaborn, plotly, statsmodels | Native; interop via `.to_pandas()` |
|
|
49
|
+
| API stability | Mature, huge Stack Overflow corpus | Younger, faster-moving |
|
|
50
|
+
|
|
51
|
+
**Default to Pandas** when you are in sklearn / statsmodels / seaborn territory with small-to-medium data — ecosystem friction is the dominant cost. **Reach for Polars** when you are doing heavy group-bys, joins, or window functions on datasets where Pandas starts swapping or takes minutes per cell. The two libraries express the same group-by almost identically:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Pandas
|
|
55
|
+
(df
|
|
56
|
+
.groupby("customer_id")
|
|
57
|
+
.agg(total_spend=("amount", "sum"), tx_count=("amount", "count"))
|
|
58
|
+
.reset_index())
|
|
59
|
+
|
|
60
|
+
# Polars (lazy — add .collect() to execute)
|
|
61
|
+
(df.lazy()
|
|
62
|
+
.group_by("customer_id")
|
|
63
|
+
.agg(pl.col("amount").sum().alias("total_spend"),
|
|
64
|
+
pl.col("amount").count().alias("tx_count"))
|
|
65
|
+
.collect())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Mixing is fine: load with Polars, do the fast aggregation, then `.to_pandas()` right before feeding a scikit-learn estimator. Avoid the trap of half-converting the codebase — pick one as the default for a given project and document it.
|
|
69
|
+
|
|
70
|
+
### Notebook to pipeline promotion
|
|
71
|
+
|
|
72
|
+
Every piece of code starts life in a notebook. The discipline is knowing when to move it:
|
|
73
|
+
|
|
74
|
+
1. You copy-paste a cell into a second notebook → promote.
|
|
75
|
+
2. A transformation has a non-trivial branch (try/except, conditional handling) → promote.
|
|
76
|
+
3. You want to unit-test it → promote (you can't test a notebook cell cleanly).
|
|
77
|
+
|
|
78
|
+
Promotion is a four-step move: extract the cell into `src/<project>/features/engineer.py` as a typed function, add a pytest in `tests/`, replace the notebook cell with an `import`, and turn on `%autoreload 2` so subsequent edits live-reload without a kernel restart.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# src/<project>/features/engineer.py
|
|
82
|
+
import polars as pl
|
|
83
|
+
|
|
84
|
+
def add_tenure_bucket(df: pl.DataFrame, *, today: str) -> pl.DataFrame:
|
|
85
|
+
"""Bucket customers by days since signup into short / medium / long tenure."""
|
|
86
|
+
return df.with_columns(
|
|
87
|
+
((pl.lit(today).str.to_date() - pl.col("signup_date")).dt.total_days())
|
|
88
|
+
.alias("tenure_days")
|
|
89
|
+
).with_columns(
|
|
90
|
+
pl.when(pl.col("tenure_days") < 90).then(pl.lit("short"))
|
|
91
|
+
.when(pl.col("tenure_days") < 365).then(pl.lit("medium"))
|
|
92
|
+
.otherwise(pl.lit("long"))
|
|
93
|
+
.alias("tenure_bucket")
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The notebook now reads `from <project>.features.engineer import add_tenure_bucket` and the function is covered by `tests/test_engineer.py` with a six-row fixture. This is the single most important habit in a DS codebase — see `data-science-project-structure.md` for the directory layout it slots into.
|
|
98
|
+
|
|
99
|
+
### Idempotent pipeline entrypoints
|
|
100
|
+
|
|
101
|
+
The pipeline is a thin composition layer — one function per stage, each one idempotent (same inputs → same outputs, safe to rerun). It lives at `src/<project>/pipeline.py` and exposes a `main(cfg)` that a CLI wraps:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# src/<project>/pipeline.py
|
|
105
|
+
import argparse, yaml
|
|
106
|
+
from pathlib import Path
|
|
107
|
+
from <project>.ingestion import load_transactions
|
|
108
|
+
from <project>.validation import validate_schema
|
|
109
|
+
from <project>.features.engineer import build_features
|
|
110
|
+
from <project>.training import train_model
|
|
111
|
+
from <project>.evaluation import evaluate
|
|
112
|
+
from <project>.io import save_model, save_report
|
|
113
|
+
|
|
114
|
+
def run(cfg: dict) -> None:
|
|
115
|
+
run_id = cfg["run_name"]
|
|
116
|
+
raw = load_transactions(cfg["data"]["raw_path"])
|
|
117
|
+
validate_schema(raw, cfg["data"]["schema"])
|
|
118
|
+
processed = build_features(raw, cfg["features"])
|
|
119
|
+
processed.write_parquet(Path(cfg["data"]["processed_path"]))
|
|
120
|
+
model, metrics = train_model(processed, cfg["model"])
|
|
121
|
+
report = evaluate(model, processed, cfg["evaluation"])
|
|
122
|
+
save_model(model, f"models/{run_id}.joblib")
|
|
123
|
+
save_report(report, f"reports/{run_id}.html")
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
ap = argparse.ArgumentParser()
|
|
127
|
+
ap.add_argument("--config", required=True, type=Path)
|
|
128
|
+
args = ap.parse_args()
|
|
129
|
+
run(yaml.safe_load(args.config.read_text()))
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Invoke it with `uv run python -m <project>.pipeline --config configs/baseline.yaml`. Idempotence means: each stage writes to a deterministic path based on the config, and re-running over existing outputs is a no-op (or an overwrite of identical content). That property is what lets a teammate — or future-you — rerun the pipeline confidently without inspecting every intermediate.
|
|
136
|
+
|
|
137
|
+
### Where outputs go
|
|
138
|
+
|
|
139
|
+
Artifacts follow a strict directory contract so a run never scatters files:
|
|
140
|
+
|
|
141
|
+
| Artifact | Path | Notes |
|
|
142
|
+
|------------------------|-------------------------------------|------------------------------------------|
|
|
143
|
+
| Immutable source data | `data/raw/` | Never written to after initial ingest |
|
|
144
|
+
| Cached partial transforms | `data/interim/` | Safe to delete; regenerable from raw |
|
|
145
|
+
| Analysis-ready datasets | `data/processed/` | Consumed by training |
|
|
146
|
+
| Predictions | `data/processed/predictions/` | Keeps inference outputs alongside data |
|
|
147
|
+
| Trained models | `models/<run_id>.joblib` | DVC or git-lfs pointer tracked |
|
|
148
|
+
| Rendered reports | `reports/<run_id>.html` | HTML / markdown summaries |
|
|
149
|
+
| Figures | `reports/figures/<run_id>/` | PNG / SVG charts |
|
|
150
|
+
|
|
151
|
+
The rule: **paths come from config, never hard-coded in code**. `cfg["output"]["model_path"]` lives in the YAML; `"models/baseline_v1.joblib"` never appears as a string literal inside `training.py`. That is what lets a single pipeline module serve every run variant.
|
|
152
|
+
|
|
153
|
+
### When to outgrow this
|
|
154
|
+
|
|
155
|
+
This architecture covers the 0-to-100GB, one-to-three-contributors slot. Signals you are leaving that slot:
|
|
156
|
+
|
|
157
|
+
- Data no longer fits on a laptop (>100 GB, or streaming sources) → Spark, DuckDB+S3, or a warehouse-side pipeline.
|
|
158
|
+
- You need scheduled / triggered runs with retries, alerting, observability → Prefect, Dagster, or Airflow.
|
|
159
|
+
- The model must serve real-time predictions with SLA → cross over to `ml-serving-patterns.md` for online inference, feature stores, and the training-serving split.
|
|
160
|
+
- Multiple people are editing the pipeline concurrently → promote `configs/` to a registry, add a model registry (MLflow), and start writing ADRs under `docs/adr/`.
|
|
161
|
+
- The team wants experiment tracking beyond a CSV of metrics → MLflow Tracking or Weights & Biases.
|
|
162
|
+
|
|
163
|
+
Do not preemptively adopt any of these. Installing Dagster for a weekly notebook is a classic small-team failure mode — the operational tax (scheduler, DB, UI, auth) dwarfs the benefit. Graduate one piece at a time, and only when the pain is concrete. The layered shape above is deliberately the smallest coherent thing; resist making it bigger until the evidence demands it.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-science-conventions
|
|
3
|
+
description: Python coding conventions for solo data-science work — ruff for lint+format, pragmatic type hints, pyproject.toml as single config source, import ordering, module layout, naming, and docstrings
|
|
4
|
+
topics: [data-science, conventions, python, ruff, type-hints]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Solo data-science code drifts faster than any other kind of Python: half of it lives in notebooks, the other half migrates into scripts, and nothing stays stable long enough to earn a style review. Consistent conventions are the only thing that keeps cognitive load bounded when you come back to a project after two months. Encode them in tooling (`ruff`, `pyproject.toml`) so they run on save — not on willpower — and the notebook→script promotion path stays smooth instead of becoming a cleanup tax.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Use `ruff` as the single lint + format tool — `ruff format` is Black-compatible and replaces Black, so do not install both. Apply `type hints` pragmatically: typed on any function another module imports, omitted on throwaway notebook helpers. Centralize all project and tool configuration in `pyproject.toml` — one file for build metadata, dependencies, ruff, and pytest. Use `ruff`/`isort`-style import sections (stdlib → third-party → local), a flat `src/` layout with a clear module split, and docstrings sized to the consumer: one-liners for internal helpers, full Google/NumPy style for anything a teammate will call without reading the source.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### Linter + formatter (ruff)
|
|
16
|
+
|
|
17
|
+
`ruff` is the only Python linter/formatter a solo DS project needs. It replaces `flake8`, `isort`, `pyupgrade`, `pydocstyle`, `pylint` (mostly), and — via `ruff format` — Black. It is an order of magnitude faster than the tools it replaces, configured in one `[tool.ruff]` block, and has no plugin-management overhead. Do not layer Black on top: `ruff format` implements the same formatting contract, and running both just causes churn.
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
# pyproject.toml
|
|
21
|
+
[tool.ruff]
|
|
22
|
+
line-length = 100
|
|
23
|
+
target-version = "py311"
|
|
24
|
+
extend-exclude = ["notebooks/_scratch", "data", "models"]
|
|
25
|
+
|
|
26
|
+
[tool.ruff.lint]
|
|
27
|
+
select = [
|
|
28
|
+
"E", # pycodestyle errors
|
|
29
|
+
"W", # pycodestyle warnings
|
|
30
|
+
"F", # pyflakes
|
|
31
|
+
"I", # isort (import sorting)
|
|
32
|
+
"N", # pep8-naming
|
|
33
|
+
"UP", # pyupgrade
|
|
34
|
+
"B", # flake8-bugbear
|
|
35
|
+
"C90", # mccabe complexity
|
|
36
|
+
"D", # pydocstyle
|
|
37
|
+
]
|
|
38
|
+
ignore = [
|
|
39
|
+
"D100", # missing docstring in public module — noisy for scripts
|
|
40
|
+
"D104", # missing docstring in public package
|
|
41
|
+
"E501", # line-too-long — formatter handles it
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.per-file-ignores]
|
|
45
|
+
# Notebooks and experiment scripts get a lighter hand
|
|
46
|
+
"notebooks/**/*.py" = ["D", "N806", "E402"]
|
|
47
|
+
"scripts/**/*.py" = ["D"]
|
|
48
|
+
"tests/**/*.py" = ["D"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint.pydocstyle]
|
|
51
|
+
convention = "google"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint.mccabe]
|
|
54
|
+
max-complexity = 12
|
|
55
|
+
|
|
56
|
+
[tool.ruff.format]
|
|
57
|
+
quote-style = "double"
|
|
58
|
+
indent-style = "space"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Tradeoff**: notebook and exploration code legitimately breaks rules that production code should not — uppercase variable names (`X_train`), imports after executable code, no docstrings. The `per-file-ignores` block disables the rules that fight notebook workflows without weakening the defaults for `src/`. Do not globally ignore `D` or `N` just to silence notebook noise.
|
|
62
|
+
|
|
63
|
+
Run on save (editor integration) and as a pre-commit hook. In CI, run `ruff check .` and `ruff format --check .` — the `--check` flag fails instead of rewriting.
|
|
64
|
+
|
|
65
|
+
### Type hints
|
|
66
|
+
|
|
67
|
+
Python is not a typed language, and pretending it is in exploratory code wastes time. The rule is **import boundary = type boundary**: if another module imports the function, type it. Notebook-local helpers and inline lambdas do not need annotations.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# src/features/encoders.py — imported by training and serving, fully typed
|
|
71
|
+
from __future__ import annotations
|
|
72
|
+
|
|
73
|
+
import numpy as np
|
|
74
|
+
import pandas as pd
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def target_encode(
|
|
78
|
+
series: pd.Series,
|
|
79
|
+
target: pd.Series,
|
|
80
|
+
smoothing: float = 10.0,
|
|
81
|
+
) -> pd.Series:
|
|
82
|
+
"""Smoothed target encoding for a categorical feature.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
series: Categorical feature values (any hashable dtype).
|
|
86
|
+
target: Numeric target aligned to `series` by index.
|
|
87
|
+
smoothing: Prior weight; higher values pull rare categories
|
|
88
|
+
toward the global mean.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Series of encoded floats aligned to `series.index`.
|
|
92
|
+
"""
|
|
93
|
+
global_mean = target.mean()
|
|
94
|
+
agg = target.groupby(series).agg(["mean", "count"])
|
|
95
|
+
weight = agg["count"] / (agg["count"] + smoothing)
|
|
96
|
+
encoding = weight * agg["mean"] + (1 - weight) * global_mean
|
|
97
|
+
return series.map(encoding).astype(np.float64)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# notebooks/03_eda.py — throwaway scratch, no annotations needed
|
|
102
|
+
def quick_hist(col):
|
|
103
|
+
return df[col].value_counts().head(20)
|
|
104
|
+
|
|
105
|
+
for c in cat_cols:
|
|
106
|
+
print(c, quick_hist(c).to_dict())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Practical rules:
|
|
110
|
+
- Type every function exported from `src/` — parameters and return.
|
|
111
|
+
- Type dataclasses and `TypedDict` schemas that describe data contracts (row shapes, config dicts).
|
|
112
|
+
- Skip annotations on notebook cells, inline closures, and private helpers inside a single script.
|
|
113
|
+
- Use `from __future__ import annotations` at the top of every `src/` file — it makes all annotations lazy strings, so forward references and expensive-to-import types (`torch.Tensor`, `pd.DataFrame`) cost nothing at import time.
|
|
114
|
+
- Do not run `mypy --strict` on a solo DS project. Run it on `src/` with `--ignore-missing-imports` if you want a safety net, and do not bother with notebooks.
|
|
115
|
+
|
|
116
|
+
### Project layout and pyproject.toml
|
|
117
|
+
|
|
118
|
+
One `pyproject.toml` at the repo root configures the build, dependencies, lint, format, and tests. Do not scatter config across `setup.cfg`, `.flake8`, `.isort.cfg`, and `pytest.ini` — everything lives in `pyproject.toml`.
|
|
119
|
+
|
|
120
|
+
```toml
|
|
121
|
+
# pyproject.toml
|
|
122
|
+
[build-system]
|
|
123
|
+
requires = ["hatchling"]
|
|
124
|
+
build-backend = "hatchling.build"
|
|
125
|
+
|
|
126
|
+
[project]
|
|
127
|
+
name = "churn-model"
|
|
128
|
+
version = "0.1.0"
|
|
129
|
+
description = "Customer churn prediction — feature pipeline, training, and serving."
|
|
130
|
+
requires-python = ">=3.11"
|
|
131
|
+
dependencies = [
|
|
132
|
+
"pandas>=2.1",
|
|
133
|
+
"numpy>=1.26",
|
|
134
|
+
"scikit-learn>=1.4",
|
|
135
|
+
"pydantic>=2.5",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
[project.optional-dependencies]
|
|
139
|
+
dev = [
|
|
140
|
+
"ruff>=0.3",
|
|
141
|
+
"pytest>=8.0",
|
|
142
|
+
"pytest-cov>=4.1",
|
|
143
|
+
"ipykernel>=6.29",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
[tool.ruff]
|
|
147
|
+
line-length = 100
|
|
148
|
+
target-version = "py311"
|
|
149
|
+
# ... (see ruff section above)
|
|
150
|
+
|
|
151
|
+
[tool.pytest.ini_options]
|
|
152
|
+
testpaths = ["tests"]
|
|
153
|
+
addopts = "-ra --strict-markers --cov=src --cov-report=term-missing"
|
|
154
|
+
markers = [
|
|
155
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
156
|
+
]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Repo layout:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
churn-model/
|
|
163
|
+
pyproject.toml
|
|
164
|
+
README.md
|
|
165
|
+
src/
|
|
166
|
+
churn_model/
|
|
167
|
+
__init__.py
|
|
168
|
+
data/ # loaders, schemas, splits
|
|
169
|
+
features/ # transformers, encoders, selection
|
|
170
|
+
models/ # model definitions and wrappers
|
|
171
|
+
training/ # train loops, CV runners
|
|
172
|
+
evaluation/ # metrics, diagnostics
|
|
173
|
+
serving/ # inference helpers
|
|
174
|
+
notebooks/
|
|
175
|
+
01_data_audit.ipynb
|
|
176
|
+
02_feature_exploration.ipynb
|
|
177
|
+
tests/
|
|
178
|
+
test_features.py
|
|
179
|
+
test_training.py
|
|
180
|
+
configs/
|
|
181
|
+
base.yaml
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use a `src/` layout (not flat) so imports always go through the installed package — this prevents the "works in notebook, breaks in test" failure mode where `from my_module import x` resolves from the CWD instead of the package.
|
|
185
|
+
|
|
186
|
+
### Import ordering
|
|
187
|
+
|
|
188
|
+
`ruff` with rule `I` enforces `isort`-compatible sections automatically. The contract:
|
|
189
|
+
|
|
190
|
+
1. Future imports (`from __future__ import annotations`)
|
|
191
|
+
2. Standard library
|
|
192
|
+
3. Third-party
|
|
193
|
+
4. First-party (your package)
|
|
194
|
+
5. Local relative (`from .utils import ...`)
|
|
195
|
+
|
|
196
|
+
One blank line between sections, alphabetical within each. Do not hand-maintain this — `ruff check --fix` sorts imports in milliseconds.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from __future__ import annotations
|
|
200
|
+
|
|
201
|
+
import json
|
|
202
|
+
from pathlib import Path
|
|
203
|
+
|
|
204
|
+
import numpy as np
|
|
205
|
+
import pandas as pd
|
|
206
|
+
from sklearn.model_selection import KFold
|
|
207
|
+
|
|
208
|
+
from churn_model.data import load_raw
|
|
209
|
+
from churn_model.features import target_encode
|
|
210
|
+
|
|
211
|
+
from .utils import timed
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Naming and docstrings
|
|
215
|
+
|
|
216
|
+
Naming rubric (enforced by `ruff` rule `N`):
|
|
217
|
+
|
|
218
|
+
- **Modules/files**: `snake_case.py` (`feature_store.py`, not `FeatureStore.py`).
|
|
219
|
+
- **Functions/variables**: `snake_case` (`compute_auc`, `n_splits`).
|
|
220
|
+
- **Classes**: `PascalCase` (`ChurnDataset`, `TargetEncoder`).
|
|
221
|
+
- **Constants**: `UPPER_SNAKE_CASE` at module top level (`DEFAULT_SEED = 42`, `FEATURE_COLUMNS: tuple[str, ...] = (...)`).
|
|
222
|
+
- **Private**: single leading underscore (`_internal_helper`). Double underscore only when you specifically want name-mangling inside a class.
|
|
223
|
+
- **Type variables**: `PascalCase` with suffix (`ModelT = TypeVar("ModelT")`).
|
|
224
|
+
- **DataFrame matrices**: `X`, `y`, `X_train`, `y_test` are the one permitted uppercase exception — this is ML convention and `ruff` can be told to allow it via `N806` ignore in model/training modules.
|
|
225
|
+
|
|
226
|
+
Docstring style sizing — match the cost of writing the docstring to the consumer:
|
|
227
|
+
|
|
228
|
+
- **Terse one-liner** for private helpers and obvious utilities. `"""Return the 95th percentile of non-null values."""` is enough.
|
|
229
|
+
- **Full Google-style** (Args/Returns/Raises) for any public function in `src/features/`, `src/models/`, or `src/serving/` — anything a teammate or future-you will call without opening the source. See the `target_encode` example above.
|
|
230
|
+
- **Module docstring** on every `src/` module: one sentence describing what lives there. Skip on `scripts/` and `notebooks/`.
|
|
231
|
+
- **Class docstring** covers the class contract; `__init__` args go in the class docstring, not a separate `__init__` docstring. (This is the Google convention and `ruff`'s `pydocstyle` setting enforces it.)
|
|
232
|
+
|
|
233
|
+
Pick Google **or** NumPy style — not both — and set it in `[tool.ruff.lint.pydocstyle]`. Google is more compact and reads better in IDE hover; NumPy is better when you have long parameter descriptions with math. For solo DS, Google is the default recommendation.
|