Skip to content

Commit 60a8969

Browse files
chore: clean remaining ruff/mypy findings and enforce lint in CI (#10)
* Add files via upload * Add files via upload
1 parent f1348fd commit 60a8969

6 files changed

Lines changed: 55 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,34 @@ on:
66
workflow_dispatch:
77

88
jobs:
9+
lint:
10+
name: Lint and type-check
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Check out repository
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.12"
21+
cache: "pip"
22+
23+
- name: Install tooling
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install ruff black mypy
27+
28+
- name: Ruff
29+
run: ruff check src app.py tests
30+
31+
- name: Black
32+
run: black --check src app.py tests
33+
34+
- name: Mypy
35+
run: mypy src app.py
36+
937
test-and-smoke:
1038
name: Test and smoke workflow on Python ${{ matrix.python-version }}
1139
runs-on: ubuntu-latest

app.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616
if str(PROJECT_ROOT) not in sys.path:
1717
sys.path.append(str(PROJECT_ROOT))
1818

19-
from src.config import METRICS_DIR, PROCESSED_DATA_DIR, TARGET_COL # type: ignore
20-
from src.dashboard_utils import ( # type: ignore
19+
from src.config import METRICS_DIR, PROCESSED_DATA_DIR, TARGET_COL # noqa: E402
20+
from src.dashboard_utils import ( # noqa: E402
2121
add_risk_band,
2222
build_model_metadata,
2323
format_float,
2424
format_percent,
2525
summarize_scored_transactions,
2626
)
27-
from src.reason_codes import shap_reason_codes, split_reason_codes # type: ignore
28-
from src.score_new_transactions import score_dataframe # type: ignore
29-
from src.validation import ( # type: ignore
27+
from src.reason_codes import shap_reason_codes, split_reason_codes # noqa: E402
28+
from src.score_new_transactions import score_dataframe # noqa: E402
29+
from src.validation import ( # noqa: E402
3030
REQUIRED_FEATURE_COLUMNS,
3131
DataValidationError,
3232
validate_scoring_dataframe,
@@ -148,19 +148,13 @@ def explain_single_transaction(model, df_scored: pd.DataFrame, row_idx: int):
148148
try:
149149
import scipy.sparse as sp
150150

151-
if sp.issparse(x_transformed):
152-
x_for_shap = x_transformed.toarray()
153-
else:
154-
x_for_shap = x_transformed
151+
x_for_shap = x_transformed.toarray() if sp.issparse(x_transformed) else x_transformed
155152
except ImportError:
156153
x_for_shap = x_transformed
157154

158155
shap_vals = explainer.shap_values(x_for_shap)
159156

160-
if isinstance(shap_vals, list):
161-
shap_for_fraud_class = shap_vals[1][0]
162-
else:
163-
shap_for_fraud_class = shap_vals[0]
157+
shap_for_fraud_class = shap_vals[1][0] if isinstance(shap_vals, list) else shap_vals[0]
164158

165159
fig = plot_single_shap_bar(shap_for_fraud_class, feature_names)
166160
reasons = shap_reason_codes(shap_for_fraud_class, feature_names, max_reasons=5)

src/evaluate.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ def compute_baseline_metrics(
113113
else:
114114
proba = clf.predict(X_test)
115115

116-
metrics = compute_probability_metrics(y_test.to_numpy(), proba)
116+
metrics: dict[str, float | str] = dict(
117+
compute_probability_metrics(y_test.to_numpy(), proba)
118+
)
117119
metrics["strategy"] = strategy
118120
baselines[name] = metrics
119121

src/explain.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ def compute_and_plot_global_shap(
3434
For tree models (RandomForest), TreeExplainer is appropriate.
3535
"""
3636
# Subsample for speed if needed
37-
if len(X) > max_samples:
38-
X_sample = X.sample(max_samples, random_state=0)
39-
else:
40-
X_sample = X
37+
X_sample = X.sample(max_samples, random_state=0) if len(X) > max_samples else X
4138

4239
# Extract underlying estimator after preprocessing
4340
preprocessor = model.named_steps["preprocess"]
@@ -46,10 +43,7 @@ def compute_and_plot_global_shap(
4643
X_transformed = preprocessor.transform(X_sample)
4744

4845
# Convert sparse matrices to dense for SHAP summary plot
49-
if sp.issparse(X_transformed):
50-
X_for_shap = X_transformed.toarray()
51-
else:
52-
X_for_shap = X_transformed
46+
X_for_shap = X_transformed.toarray() if sp.issparse(X_transformed) else X_transformed
5347

5448
explainer = shap.TreeExplainer(clf)
5549
shap_values = explainer.shap_values(X_for_shap)

src/generate_synthetic_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ def generate_synthetic_fraud_dataset(
182182
device_risk_score[low_risk_fraud] = rng.beta(1.5, 4.5, size=int(low_risk_fraud.sum()))
183183
ip_risk_score[low_risk_fraud] = rng.beta(1.5, 4.2, size=int(low_risk_fraud.sum()))
184184

185-
device_risk_score = np.round(np.clip(device_risk_score, 0, 1), 4)
186-
ip_risk_score = np.round(np.clip(ip_risk_score, 0, 1), 4)
185+
device_risk_score = np.round(np.clip(device_risk_score, 0, 1), 4).astype(float)
186+
ip_risk_score = np.round(np.clip(ip_risk_score, 0, 1), 4).astype(float)
187187

188188
transaction_type = _sample_by_class(
189189
rng,

src/threshold_policy.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def _policy_row(name: str, row: dict[str, Any], rationale: str) -> dict[str, Any
3333
"tn",
3434
"fn",
3535
]
36-
result = {"policy": name, "rationale": rationale}
36+
result: dict[str, Any] = {"policy": name, "rationale": rationale}
3737
for key in keys:
3838
result[key] = row.get(key)
3939
return result
@@ -91,7 +91,8 @@ def add(name: str, row: dict[str, Any] | None, rationale: str) -> None:
9191
add(
9292
"cost_optimized",
9393
cost_optimized,
94-
"Minimizes expected business cost under the configured false-positive and false-negative costs.",
94+
"Minimizes expected business cost under the configured "
95+
"false-positive and false-negative costs.",
9596
)
9697

9798
balanced_f1 = _max_by(
@@ -135,7 +136,8 @@ def add(name: str, row: dict[str, Any] | None, rationale: str) -> None:
135136
add(
136137
"high_precision",
137138
high_precision,
138-
f"Maintains precision of at least {target_precision:.0%} while preserving as much recall as possible.",
139+
f"Maintains precision of at least {target_precision:.0%} "
140+
"while preserving as much recall as possible.",
139141
)
140142

141143
review_capacity_pool = [
@@ -182,8 +184,10 @@ def build_threshold_policy_summary(
182184
"policy_candidates": candidates,
183185
"notes": [
184186
"These policies are decision-support artifacts, not automatic approval rules.",
185-
"Thresholds should be reviewed with business, compliance, and operations stakeholders before deployment.",
186-
"The demo dataset is synthetic; threshold values should not be reused for real banking data without validation.",
187+
"Thresholds should be reviewed with business, compliance, and operations "
188+
"stakeholders before deployment.",
189+
"The demo dataset is synthetic; threshold values should not be reused "
190+
"for real banking data without validation.",
187191
],
188192
}
189193

@@ -260,7 +264,10 @@ def _render_markdown_policy(summary: dict[str, Any]) -> str:
260264

261265
for row in candidates:
262266
lines.append(
263-
"| {policy} | {threshold} | {precision} | {recall} | {fpr} | {flagged} | {cost} | {rationale} |".format(
267+
(
268+
"| {policy} | {threshold} | {precision} | {recall} | "
269+
"{fpr} | {flagged} | {cost} | {rationale} |"
270+
).format(
264271
policy=row.get("policy", "n/a"),
265272
threshold=_format_rate(row.get("threshold")),
266273
precision=_format_rate(row.get("precision")),

0 commit comments

Comments
 (0)