Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ project_root/
├── run_pipeline.py # Entrypoint: orchestrates ETL
├── src/
│ ├── config.py # Paths, constants, env vars
│ ├── db/connection.py # DB abstraction (DuckDB now, BQ later)
│ ├── db/connection.py # DB abstraction (DuckDB now, BigQuery later)
│ ├── pipeline/
│ │ ├── step_01_ingest.py # CSV OULAD → raw DuckDB tables
│ │ ├── step_02_transform.py # Raw tables → analytical views
Expand Down Expand Up @@ -171,7 +171,7 @@ contains 32,593 course enrollments by 28,785 distinct students across
| studentVle | Clickstream (daily clicks per resource) | id_site, date, sum_click |
| studentAssessment | Assessment scores | id_assessment, score |
| assessments | Assessment metadata | assessment_type, date, weight |
| vle | VLE resource metadata | activity_type |
| vle | Virtual Learning Environment (VLE) resource metadata | activity_type |
| courses | Course metadata | module_presentation_length |

**Target variable**: `final_result` ∈ {Pass, Distinction, Fail, Withdrawn},
Expand Down
4 changes: 2 additions & 2 deletions it/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ project_root/
├── run_pipeline.py # Entrypoint: orchestra l'ETL
├── src/
│ ├── config.py # Path, costanti, variabili d'ambiente
│ ├── db/connection.py # Astrazione DB (DuckDB ora, BQ in futuro)
│ ├── db/connection.py # Astrazione DB (DuckDB ora, BigQuery in futuro)
│ ├── pipeline/
│ │ ├── step_01_ingest.py # CSV OULAD → tabelle raw DuckDB
│ │ ├── step_02_transform.py # Tabelle raw → viste analitiche
Expand Down Expand Up @@ -171,7 +171,7 @@ su 7 moduli (22 presentazioni) presso la Open University (UK).
| studentVle | Clickstream (click giornalieri per risorsa) | id_site, date, sum_click |
| studentAssessment | Punteggi delle valutazioni | id_assessment, score |
| assessments | Metadati delle valutazioni | assessment_type, date, weight |
| vle | Metadati risorse VLE | activity_type |
| vle | Metadati risorse VLE (Virtual Learning Environment) | activity_type |
| courses | Metadati dei corsi | module_presentation_length |

**Variabile target**: `final_result` ∈ {Pass, Distinction, Fail, Withdrawn},
Expand Down
92 changes: 46 additions & 46 deletions notebooks/01_eda_student_base.ipynb

Large diffs are not rendered by default.

104 changes: 52 additions & 52 deletions notebooks/02_eda_engagement_patterns.ipynb

Large diffs are not rendered by default.

74 changes: 37 additions & 37 deletions notebooks/03_bq1_dropout_timing.ipynb

Large diffs are not rendered by default.

58 changes: 29 additions & 29 deletions notebooks/04_bq2_early_signals.ipynb

Large diffs are not rendered by default.

70 changes: 35 additions & 35 deletions notebooks/05_bq3_demographics_vs_behavior.ipynb

Large diffs are not rendered by default.

57 changes: 32 additions & 25 deletions notebooks/06_bq4_course_comparison.ipynb

Large diffs are not rendered by default.

102 changes: 51 additions & 51 deletions notebooks/07_bq5_recommendations_synthesis.ipynb

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions reports/REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

This report synthesizes findings from a SQL-driven analytical pipeline applied to the
OULAD dataset: 32,593 student-course enrollments across 7 modules, with complete
behavioral clickstream, assessment records, and demographic profiles.
behavioral clickstream from the university's Virtual Learning Environment (VLE),
assessment records, and demographic profiles.

**Outcome definition:** Each enrollment is classified as *Completed* (Pass or Distinction)
or *Not completed* (Fail or Withdrawn). This binary split is consistent with the OULAD
Expand Down Expand Up @@ -138,8 +139,8 @@ These behavioral signals are strong. But are they merely proxies for demographic
> than demographic effect sizes. Within every education level, high engagement
> dramatically outperforms low engagement.

We tested 6 categorical demographic features (gender, age band, education level, IMD
band, disability, region) and 2 numeric demographic features (previous attempts, studied
We tested 6 categorical demographic features (gender, age band, education level,
Index of Multiple Deprivation (IMD) band, disability, region) and 2 numeric demographic features (previous attempts, studied
credits) against completion outcome. All 8 are statistically significant after
Benjamini-Hochberg correction, but their effect sizes are uniformly weak. The strongest
demographic predictor (highest education) reaches a Cramer's V of approximately **0.15**;
Expand Down
Binary file modified reports/figures/02_course_engagement_boxplot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/02_decile_completion_rate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/02_decile_outcome_stacked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/02_engagement_typology_scatter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/02_ghost_outcome_distribution.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/03_course_design_vs_dropout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/03_dropout_by_demographics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/03_dropout_cliffs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/03_dropout_curves_overlaid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/03_dropout_normalized.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/05_behavior_effect_sizes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/05_demographic_completion_rates.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/05_demographics_vs_behavior_comparison.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/06_course_completion_ranking.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/06_course_design_heatmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/06_engagement_by_course.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/07_priority_matrix.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/07_segment_overlap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified reports/figures/07_segment_sizing_overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions reports/it/REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

Questo report sintetizza i risultati di una pipeline analitica SQL-driven applicata al
dataset OULAD: 32.593 iscrizioni studente-corso distribuite su 7 moduli, con clickstream
comportamentale completo, record delle valutazioni e profili demografici.
comportamentale completo dal Virtual Learning Environment (VLE) dell'università, record
delle valutazioni e profili demografici.

**Definizione dell'outcome:** Ogni iscrizione è classificata come *Completato* (Pass o
Distinction) o *Non completato* (Fail o Withdrawn). Questa suddivisione binaria è
Expand Down Expand Up @@ -145,7 +146,8 @@ Questi segnali comportamentali sono forti. Ma sono semplicemente proxy della dem
> l'engagement alto supera drammaticamente l'engagement basso.

Abbiamo testato 6 variabili demografiche categoriche (genere, fascia d'età, livello di
istruzione, fascia IMD, disabilità, regione) e 2 variabili demografiche numeriche
istruzione, fascia IMD (Index of Multiple Deprivation), disabilità, regione) e 2
variabili demografiche numeriche
(tentativi precedenti, crediti studiati) contro l'esito di completamento. Tutte le 8 sono
statisticamente significative dopo correzione Benjamini-Hochberg, ma i loro effect size
sono uniformemente deboli. Il predittore demografico più forte (livello di istruzione
Expand Down
12 changes: 6 additions & 6 deletions run_pipeline.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Pipeline orchestrator runs all ETL steps, or a single one via --step.
"""Pipeline orchestrator: runs all ETL steps, or a single one via --step.

Entry point for the entire analytical pipeline:
python -m run_pipeline # all steps, full dataset
Expand Down Expand Up @@ -29,7 +29,7 @@
def main() -> None:
"""Parse arguments and run the full pipeline."""
parser = argparse.ArgumentParser(
description="Learning Retention Analytics ETL Pipeline"
description="Learning Retention Analytics: ETL Pipeline"
)
parser.add_argument(
"--step",
Expand Down Expand Up @@ -62,21 +62,21 @@ def main() -> None:
run_step: str | None = args.step

logger.info(
"Pipeline starting source: %s, step: %s",
"Pipeline starting (source: %s, step: %s)",
"data_sample" if args.sample else "data/raw",
run_step or "all",
)

if run_step in (None, "ingest"):
with step_timer("Step 01 Ingest"):
with step_timer("Step 01: Ingest"):
ingest(use_sample=args.sample)

if run_step in (None, "transform"):
with step_timer("Step 02 Transform"):
with step_timer("Step 02: Transform"):
transform()

if run_step in (None, "export"):
with step_timer("Step 03 Export"):
with step_timer("Step 03: Export"):
export()

if run_step in (None, "stats"):
Expand Down
2 changes: 1 addition & 1 deletion sql/queries/q_bq1_dropout_curves.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- q_bq1_dropout_curves BQ1: Where and when do students drop out?
-- q_bq1_dropout_curves (BQ1): Where and when do students drop out?
--
-- Computes cumulative dropout curves per course-presentation.
-- Each row represents a day when at least one dropout occurred,
Expand Down
6 changes: 3 additions & 3 deletions sql/queries/q_bq2_early_signals.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- q_bq2_early_signals BQ2: Which early behavioral signals predict drop-out?
-- q_bq2_early_signals (BQ2): Which early behavioral signals predict drop-out?
--
-- Joins early engagement metrics (first 28 days) with student outcomes
-- to create a dataset ready for statistical testing in Python.
Expand All @@ -8,7 +8,7 @@
-- then runs t-tests comparing completed=1 vs completed=0 groups on each
-- engagement metric, with effect sizes and multiple comparison correction.
--
-- This query intentionally does NOT perform the statistical tests in SQL
-- This query intentionally does NOT perform the statistical tests in SQL;
-- those require scipy/statsmodels and are better handled in Python.
-- SQL's role here is to prepare the clean, joined dataset.

Expand Down Expand Up @@ -37,7 +37,7 @@ SELECT
FROM v_student_enriched se

-- LEFT JOIN because some students may have zero VLE activity in the first 28 days
-- (they enrolled but never clicked anything a strong dropout signal itself)
-- (they enrolled but never clicked anything, a strong dropout signal itself)
LEFT JOIN v_engagement_early ee
ON se.id_student = ee.id_student
AND se.code_module = ee.code_module
Expand Down
2 changes: 1 addition & 1 deletion sql/queries/q_bq3_demographics_vs_behavior.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- q_bq3_demographics_vs_behavior BQ3: Demographics vs behavior as outcome predictors
-- q_bq3_demographics_vs_behavior (BQ3): Demographics vs behavior as outcome predictors
--
-- Combines demographic features from studentInfo with behavioral features
-- from early engagement to create a dataset for comparative analysis.
Expand Down
2 changes: 1 addition & 1 deletion sql/queries/q_bq4_course_comparison.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- q_bq4_course_comparison BQ4: How do course characteristics affect retention?
-- q_bq4_course_comparison (BQ4): How do course characteristics affect retention?
--
-- Aggregates course-level metrics to compare retention across the 7 OULAD modules.
-- Each row represents one module (aggregated across all presentations) with:
Expand Down
8 changes: 4 additions & 4 deletions sql/queries/q_bq5_segment_sizing.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
-- q_bq5_segment_sizing BQ5: Sizing the target segments for interventions
-- q_bq5_segment_sizing (BQ5): Sizing the target segments for interventions
--
-- Quantifies the student segments that the top 3 interventions would target.
-- Each segment is defined by observable, actionable criteria (not demographics)
-- so that a platform operator can implement automated triggers.
--
-- The three segments correspond to the expected BQ5 recommendations:
-- 1. "Ghost students" enrolled but zero/minimal VLE activity in week 1-4
-- 2. "Assessment non-submitters" didn't submit the first assessment
-- 3. "Early disengagers" active initially but activity drops to zero by week 3-4
-- 1. "Ghost students": enrolled but zero/minimal VLE activity in week 1-4
-- 2. "Assessment non-submitters": didn't submit the first assessment
-- 3. "Early disengagers": active initially but activity drops to zero by week 3-4
--
-- This query sizes each segment and computes their dropout rates to
-- estimate the impact of targeted interventions.
Expand Down
2 changes: 1 addition & 1 deletion sql/schema.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- schema.sql DDL for OULAD raw tables
-- schema.sql: DDL for OULAD raw tables
--
-- Defines the 7 raw tables that mirror the OULAD CSV structure.
-- All types are kept generic (VARCHAR, INTEGER, DOUBLE) for ANSI compliance.
Expand Down
2 changes: 1 addition & 1 deletion sql/views/v_course_profile.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- v_course_profile Course-level characteristics and retention metrics
-- v_course_profile: Course-level characteristics and retention metrics
--
-- One row per course-presentation with aggregated metrics that characterize
-- the course design and student outcomes. Used in BQ4 ("how do course
Expand Down
4 changes: 2 additions & 2 deletions sql/views/v_dropout_timing.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
-- v_dropout_timing When students drop out relative to course timeline
-- v_dropout_timing: When students drop out relative to course timeline
--
-- Enriches each withdrawal event with course context (total duration,
-- percentage through the course) to answer BQ1: "where and when do
-- students drop out?"
--
-- Only includes students who explicitly withdrew (date_unregistration IS NOT NULL).
-- Students who failed but never withdrew are excluded they represent
-- Students who failed but never withdrew are excluded: they represent
-- a different phenomenon (academic failure vs. active departure).
--
-- The dropout_pct field normalizes timing across courses of different lengths,
Expand Down
2 changes: 1 addition & 1 deletion sql/views/v_engagement_daily.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- v_engagement_daily Daily engagement aggregates per student
-- v_engagement_daily: Daily engagement aggregates per student
--
-- Aggregates the raw clickstream (studentVle) to one row per student per day,
-- summing clicks across all VLE resources and counting distinct resource types.
Expand Down
2 changes: 1 addition & 1 deletion sql/views/v_engagement_early.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- v_engagement_early Engagement metrics for the first 28 days (0-28)
-- v_engagement_early: Engagement metrics for the first 28 days (0-28)
--
-- Aggregates clickstream data from the first 4 weeks of each course
-- to create early behavioral signals per student. This is the key view
Expand Down
2 changes: 1 addition & 1 deletion sql/views/v_student_enriched.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- v_student_enriched Core student-level view with enriched outcome data
-- v_student_enriched: Core student-level view with enriched outcome data
--
-- Combines studentInfo (demographics + final result) with studentRegistration
-- (enrollment/withdrawal dates) to create a single row per student-module
Expand Down
2 changes: 1 addition & 1 deletion src/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Centralized configuration paths, constants, and env vars.
"""Centralized configuration: paths, constants, and env vars.

All paths are relative to PROJECT_ROOT.
Single env var: PUSH_TO_SHEETS (default false).
Expand Down
4 changes: 2 additions & 2 deletions src/db/connection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Database abstraction layer DuckDB now, BigQuery later.
"""Database abstraction layer: DuckDB now, BigQuery later.

All database access in the project MUST go through this module.
No direct duckdb.connect() calls elsewhere in the codebase.
Expand Down Expand Up @@ -38,7 +38,7 @@ def get_connection(
duckdb.DuckDBPyConnection
"""
if db_path is None:
# DuckDB does not support read_only on :memory: connections
# DuckDB does not support read_only on :memory: connections;
# the parameter is silently ignored. This is expected: in-memory
# DBs are ephemeral test fixtures, not shared resources.
logger.debug("Opening in-memory DuckDB connection")
Expand Down
4 changes: 2 additions & 2 deletions src/pipeline/step_01_ingest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Step 01 Ingest OULAD CSV files into raw DuckDB tables.
"""Step 01: Ingest OULAD CSV files into raw DuckDB tables.

Reads the 7 OULAD CSV files from data/raw/ (or data_sample/) and loads
them into DuckDB tables defined by sql/schema.sql.

This step is idempotent: running it again will DROP and re-CREATE all tables,
ensuring a clean state. This is acceptable because raw data is always
available on disk as CSVs the DuckDB tables are a derived artifact.
available on disk as CSVs; the DuckDB tables are a derived artifact.
"""

import logging
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline/step_02_transform.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Step 02 Transform raw tables into analytical views.
"""Step 02: Transform raw tables into analytical views.

Executes all SQL view definitions from sql/views/ against the DuckDB database.
Views are the analytical backbone of the project: they encapsulate the business
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline/step_03_export.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Step 03 Export analytical views to CSV and optionally to Google Sheets.
"""Step 03: Export analytical views to CSV and optionally to Google Sheets.

Materializes each analytical view into a CSV file under data/analysis/.
When PUSH_TO_SHEETS is enabled, the same data is also pushed to Google Sheets
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline/step_04_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

# Significance threshold for the significant_* flags in the exported CSVs.
# 0.05 matches the notebooks; the flags are a convenience for dashboard
# consumers effect size remains the primary ranking criterion.
# consumers; effect size remains the primary ranking criterion.
ALPHA: float = 0.05

# Bootstrap resamples for the ghost/active completion-rate CIs.
Expand Down
2 changes: 1 addition & 1 deletion src/sheets/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _get_credentials_from_keychain() -> dict:
SHEETS_KEYCHAIN_SERVICE, SHEETS_KEYCHAIN_ACCOUNT
)
if not raw:
# Generic error message never reveal what we expected to find
# Generic error message: never reveal what we expected to find
raise RuntimeError("Credentials not found in macOS Keychain.")

try:
Expand Down
18 changes: 9 additions & 9 deletions src/stats/tests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Statistical test wrappers t-test, chi-square, effect sizes, confidence intervals.
"""Statistical test wrappers: t-test, chi-square, effect sizes, confidence intervals.

These wrappers standardize the interface for all statistical tests used
in the project, ensuring consistent output format (test statistic, p-value,
Expand Down Expand Up @@ -71,7 +71,7 @@ def independent_t_test(
Includes t-statistic, p-value, Cohen's d, and 95% CI for
the difference in means.
"""
# Drop NaN values missing data should not influence the test
# Drop NaN values: missing data should not influence the test
g1: np.ndarray = np.asarray(group1, dtype=float)
g2: np.ndarray = np.asarray(group2, dtype=float)
g1 = g1[~np.isnan(g1)]
Expand Down Expand Up @@ -103,8 +103,8 @@ def independent_t_test(
)
# When pooled_std is zero both groups have zero variance (all values
# identical within each group). If the means also match, Cohen's d is
# genuinely 0. If the means differ, the effect is theoretically infinite
# returning 0.0 would silently hide a real difference.
# genuinely 0. If the means differ, the effect is theoretically
# infinite; returning 0.0 would silently hide a real difference.
if pooled_std == 0:
mean_g1: float = float(np.mean(g1))
mean_g2: float = float(np.mean(g2))
Expand All @@ -116,7 +116,7 @@ def independent_t_test(
cohens_d = float((np.mean(g1) - np.mean(g2)) / pooled_std)

# 95% CI for the difference in means using Welch-Satterthwaite
# degrees of freedom matches the Welch t-test above instead of
# degrees of freedom, which matches the Welch t-test above instead of
# the normal approximation (z=1.96), which undercovers for small samples
mean_diff: float = float(np.mean(g1) - np.mean(g2))
s1_sq_n: float = np.var(g1, ddof=1) / len(g1)
Expand Down Expand Up @@ -187,7 +187,7 @@ def chi_square_test(
if observed.ndim != 2 or observed.shape[0] < 2 or observed.shape[1] < 2:
raise ValueError(
f"Contingency table must be at least 2×2, got {observed.shape}. "
"A degenerate table means one variable has a single category "
"A degenerate table means one variable has a single category; "
"chi-square test is not applicable."
)

Expand All @@ -200,7 +200,7 @@ def chi_square_test(

# Cramér's V: effect size for chi-square
# Ranges from 0 (no association) to 1 (perfect association)
# k = min(rows, cols) the smaller dimension of the contingency table
# k = min(rows, cols), the smaller dimension of the contingency table
n: int = int(observed.sum())
k: int = min(observed.shape) - 1
cramers_v: float = np.sqrt(chi2 / (n * k)) if (n * k) > 0 else 0.0
Expand Down Expand Up @@ -319,7 +319,7 @@ def bootstrap_ci(
tuple[float, float]
(lower_bound, upper_bound) of the confidence interval.
"""
# Validate parameters at the boundary invalid values would
# Validate parameters at the boundary: invalid values would
# produce confusing numpy errors deeper in the computation
if not (0 < confidence < 1):
raise ValueError(
Expand All @@ -334,7 +334,7 @@ def bootstrap_ci(

# Guard: bootstrap requires at least one finite value to resample from.
# An empty array (all NaN or empty input) would produce a (nan, nan)
# interval silently better to fail explicitly.
# interval silently; better to fail explicitly.
if len(arr) == 0:
raise ValueError(
"Cannot compute bootstrap CI: no finite values remain after "
Expand Down
8 changes: 4 additions & 4 deletions src/utils/runtime.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Runtime utilities step timing and environment info.
"""Runtime utilities: step timing and environment info.

Provides a context manager for timing pipeline steps and a function
to log the current runtime environment (Python version, key library versions).
Expand All @@ -21,9 +21,9 @@ def step_timer(step_name: str) -> Generator[None, None, None]:

Usage
-----
>>> with step_timer("Step 01 Ingest"):
>>> with step_timer("Step 01: Ingest"):
... ingest()
# logs: "Step 01 Ingest completed in 3.42s"
# logs: "Step 01: Ingest completed in 3.42s"
"""
start: float = time.perf_counter()
logger.info("Starting: %s", step_name)
Expand All @@ -48,7 +48,7 @@ def log_environment() -> None:
for lib_name in ["duckdb", "pandas", "numpy", "scipy"]:
try:
lib = __import__(lib_name)
# Not all modules expose __version__ guard to avoid
# Not all modules expose __version__, so guard to avoid
# AttributeError breaking the entire startup log
version: str = getattr(lib, "__version__", "unknown")
logger.info("%s %s", lib_name, version)
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Shared test fixtures DuckDB in-memory database with sample data.
"""Shared test fixtures: DuckDB in-memory database with sample data.

All tests use an in-memory DuckDB connection loaded with data_sample/ CSVs.
This avoids touching the real database and makes tests fast and isolated.
Expand Down
2 changes: 1 addition & 1 deletion tests/test_config_stress.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Stress tests for src/config.py env var edge cases and path validation.
"""Stress tests for src/config.py: env var edge cases and path validation.

The config module is loaded once at import time. Since PUSH_TO_SHEETS is
computed at module level from os.environ, testing different env var values
Expand Down
Loading
Loading