dataleaks 0.1.0__py3-none-any.whl

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.
Files changed (54) hide show
  1. dataleaks/__init__.py +3 -0
  2. dataleaks/api.py +67 -0
  3. dataleaks/cli.py +276 -0
  4. dataleaks/detectors/__init__.py +0 -0
  5. dataleaks/detectors/cross_dataset/__init__.py +0 -0
  6. dataleaks/detectors/cross_dataset/overlap.py +137 -0
  7. dataleaks/detectors/feature/__init__.py +0 -0
  8. dataleaks/detectors/feature/identifier.py +238 -0
  9. dataleaks/detectors/feature/suspicious.py +278 -0
  10. dataleaks/detectors/feature/target_encoding.py +88 -0
  11. dataleaks/detectors/preprocessing/__init__.py +0 -0
  12. dataleaks/detectors/preprocessing/contamination.py +94 -0
  13. dataleaks/detectors/preprocessing/fit_before_split.py +83 -0
  14. dataleaks/detectors/split/__init__.py +0 -0
  15. dataleaks/detectors/split/duplicates.py +82 -0
  16. dataleaks/detectors/split/near_duplicates.py +146 -0
  17. dataleaks/detectors/split/overlap.py +254 -0
  18. dataleaks/detectors/target/__init__.py +0 -0
  19. dataleaks/detectors/target/derived.py +200 -0
  20. dataleaks/detectors/target/direct.py +60 -0
  21. dataleaks/detectors/target/statistical.py +104 -0
  22. dataleaks/detectors/temporal/__init__.py +0 -0
  23. dataleaks/detectors/temporal/future_features.py +784 -0
  24. dataleaks/detectors/temporal/parsing.py +265 -0
  25. dataleaks/detectors/temporal/time_order.py +95 -0
  26. dataleaks/engine/__init__.py +7 -0
  27. dataleaks/engine/aggregator.py +288 -0
  28. dataleaks/engine/defaults.py +341 -0
  29. dataleaks/engine/detector.py +16 -0
  30. dataleaks/engine/registry.py +97 -0
  31. dataleaks/engine/runner.py +75 -0
  32. dataleaks/recommendations/__init__.py +0 -0
  33. dataleaks/recommendations/fixes.py +42 -0
  34. dataleaks/recommendations/recommendations.py +27 -0
  35. dataleaks/reporting/__init__.py +5 -0
  36. dataleaks/reporting/console.py +93 -0
  37. dataleaks/reporting/json.py +114 -0
  38. dataleaks/reporting/report.py +224 -0
  39. dataleaks/reporting/summary.py +37 -0
  40. dataleaks/schemas/__init__.py +7 -0
  41. dataleaks/schemas/config.py +31 -0
  42. dataleaks/schemas/dataset.py +93 -0
  43. dataleaks/schemas/execution.py +86 -0
  44. dataleaks/schemas/finding.py +29 -0
  45. dataleaks/scoring/__init__.py +0 -0
  46. dataleaks/scoring/confidence.py +49 -0
  47. dataleaks/scoring/risk.py +76 -0
  48. dataleaks/scoring/severity.py +64 -0
  49. dataleaks-0.1.0.dist-info/METADATA +700 -0
  50. dataleaks-0.1.0.dist-info/RECORD +54 -0
  51. dataleaks-0.1.0.dist-info/WHEEL +5 -0
  52. dataleaks-0.1.0.dist-info/entry_points.txt +2 -0
  53. dataleaks-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. dataleaks-0.1.0.dist-info/top_level.txt +1 -0
dataleaks/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from dataleaks.api import DataLeaks
2
+
3
+ __all__ = ["DataLeaks"]
dataleaks/api.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from dataleaks.engine.aggregator import FindingAggregator
6
+ from dataleaks.engine.defaults import build_default_registry
7
+ from dataleaks.engine.registry import DetectorRegistry
8
+ from dataleaks.engine.runner import DetectorRunner
9
+ from dataleaks.reporting.report import LeakageReport
10
+ from dataleaks.schemas.config import DataLeaksConfig
11
+ from dataleaks.schemas.dataset import DatasetContext
12
+ from dataleaks.schemas.finding import Finding
13
+
14
+
15
+ class DataLeaks:
16
+ """Public API for running DataLeaks analysis."""
17
+
18
+ def __init__(
19
+ self,
20
+ data,
21
+ *,
22
+ target: str | None = None,
23
+ train=None,
24
+ validation=None,
25
+ test=None,
26
+ config: DataLeaksConfig | None = None,
27
+ metadata: dict[str, Any] | None = None,
28
+ registry: DetectorRegistry | None = None,
29
+ ) -> None:
30
+ self.context = DatasetContext(
31
+ data=data,
32
+ target=target,
33
+ train=train if train is not None else data,
34
+ validation=validation,
35
+ test=test,
36
+ metadata=metadata or {},
37
+ )
38
+
39
+ self.config = config or DataLeaksConfig()
40
+
41
+ if registry is None:
42
+ self.registry = build_default_registry(
43
+ self.context,
44
+ self.config,
45
+ )
46
+ else:
47
+ self.registry = registry
48
+
49
+ def run(self) -> LeakageReport:
50
+ """Run detectors, aggregate findings, and build a report."""
51
+
52
+ runner = DetectorRunner(self.registry)
53
+
54
+ findings: list[Finding] = runner.run(
55
+ self.context
56
+ )
57
+
58
+ findings = FindingAggregator().aggregate(
59
+ findings
60
+ )
61
+
62
+ return LeakageReport.from_findings(
63
+ findings,
64
+ metadata=self.context.metadata,
65
+ execution=runner.execution,
66
+ schema_validation=self.context.schema_validation,
67
+ )
dataleaks/cli.py ADDED
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import ast
5
+ import sys
6
+
7
+ import pandas as pd
8
+
9
+ from dataleaks.api import DataLeaks
10
+ from dataleaks.reporting.console import ConsoleReporter
11
+ from dataleaks.reporting.json import JSONReporter
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ """Build the DataLeaks command-line argument parser."""
16
+
17
+ parser = argparse.ArgumentParser(
18
+ prog="dataleaks",
19
+ description="Detect data leakage in machine learning datasets.",
20
+ )
21
+
22
+ parser.add_argument(
23
+ "file",
24
+ help="Path to the training CSV dataset.",
25
+ )
26
+
27
+ parser.add_argument(
28
+ "--target",
29
+ help="Target column used for leakage analysis.",
30
+ default=None,
31
+ )
32
+
33
+ parser.add_argument(
34
+ "--test",
35
+ help="Path to the test CSV dataset.",
36
+ default=None,
37
+ )
38
+
39
+ parser.add_argument(
40
+ "--validation",
41
+ help="Path to the validation CSV dataset.",
42
+ default=None,
43
+ )
44
+
45
+ parser.add_argument(
46
+ "--time-column",
47
+ help=(
48
+ "Time column used for chronological train/test "
49
+ "split validation."
50
+ ),
51
+ default=None,
52
+ )
53
+
54
+ parser.add_argument(
55
+ "--prediction-time-column",
56
+ help=(
57
+ "Prediction timestamp column used to detect "
58
+ "future feature timestamps."
59
+ ),
60
+ default=None,
61
+ )
62
+
63
+ parser.add_argument(
64
+ "--feature-time-columns",
65
+ nargs="+",
66
+ help=(
67
+ "One or more feature timestamp columns to compare "
68
+ "against the prediction timestamp."
69
+ ),
70
+ default=None,
71
+ )
72
+
73
+ parser.add_argument(
74
+ "--conditional-time-column",
75
+ help=(
76
+ "Temporal column whose expected presence depends "
77
+ "on a target condition."
78
+ ),
79
+ default=None,
80
+ )
81
+
82
+ parser.add_argument(
83
+ "--conditional-target-column",
84
+ help=(
85
+ "Target column controlling conditional temporal "
86
+ "missingness."
87
+ ),
88
+ default=None,
89
+ )
90
+
91
+ parser.add_argument(
92
+ "--conditional-present-when",
93
+ help=(
94
+ "Target value for which the conditional temporal "
95
+ "column is expected to be present."
96
+ ),
97
+ default=None,
98
+ )
99
+
100
+ parser.add_argument(
101
+ "--output",
102
+ choices=["console", "json"],
103
+ default="console",
104
+ help="Output format. Defaults to console.",
105
+ )
106
+
107
+ return parser
108
+
109
+
110
+ def _parse_cli_value(value: str) -> object:
111
+ """Parse a CLI scalar into a Python value when possible."""
112
+
113
+ try:
114
+ return ast.literal_eval(value)
115
+ except (ValueError, SyntaxError):
116
+ return value
117
+
118
+
119
+ def _build_temporal_metadata(
120
+ *,
121
+ time_column: str | None,
122
+ prediction_time_column: str | None,
123
+ feature_time_columns: list[str] | None,
124
+ conditional_time_column: str | None = None,
125
+ conditional_target_column: str | None = None,
126
+ conditional_present_when: str | None = None,
127
+ ) -> dict:
128
+ """Build temporal metadata from CLI arguments."""
129
+
130
+ temporal: dict[str, object] = {}
131
+
132
+ if time_column is not None:
133
+ temporal["time_column"] = time_column
134
+
135
+ if prediction_time_column is not None:
136
+ temporal["prediction_time_column"] = (
137
+ prediction_time_column
138
+ )
139
+
140
+ if feature_time_columns is not None:
141
+ temporal["feature_time_columns"] = (
142
+ feature_time_columns
143
+ )
144
+
145
+ conditional_args = (
146
+ conditional_time_column,
147
+ conditional_target_column,
148
+ conditional_present_when,
149
+ )
150
+
151
+ conditional_count = sum(
152
+ value is not None
153
+ for value in conditional_args
154
+ )
155
+
156
+ if conditional_count not in (0, 3):
157
+ raise ValueError(
158
+ "--conditional-time-column, "
159
+ "--conditional-target-column, and "
160
+ "--conditional-present-when must be provided together"
161
+ )
162
+
163
+ if conditional_count == 3:
164
+ temporal["conditional_columns"] = {
165
+ conditional_time_column: {
166
+ "target_column": conditional_target_column,
167
+ "present_when": _parse_cli_value(
168
+ conditional_present_when
169
+ ),
170
+ }
171
+ }
172
+
173
+ return temporal
174
+
175
+
176
+ def main() -> int:
177
+ """Run the DataLeaks CLI."""
178
+
179
+ parser = build_parser()
180
+ args = parser.parse_args()
181
+
182
+ try:
183
+ data = pd.read_csv(args.file)
184
+
185
+ test = (
186
+ pd.read_csv(args.test)
187
+ if args.test is not None
188
+ else None
189
+ )
190
+
191
+ validation = (
192
+ pd.read_csv(args.validation)
193
+ if args.validation is not None
194
+ else None
195
+ )
196
+
197
+ temporal_metadata = _build_temporal_metadata(
198
+ time_column=args.time_column,
199
+ prediction_time_column=(
200
+ args.prediction_time_column
201
+ ),
202
+ feature_time_columns=args.feature_time_columns,
203
+ conditional_time_column=(
204
+ args.conditional_time_column
205
+ ),
206
+ conditional_target_column=(
207
+ args.conditional_target_column
208
+ ),
209
+ conditional_present_when=(
210
+ args.conditional_present_when
211
+ ),
212
+ )
213
+
214
+ metadata = {}
215
+
216
+ if temporal_metadata:
217
+ metadata["temporal"] = temporal_metadata
218
+
219
+ report = DataLeaks(
220
+ data,
221
+ target=args.target,
222
+ test=test,
223
+ validation=validation,
224
+ metadata=metadata,
225
+ ).run()
226
+
227
+ if args.output == "json":
228
+ output = JSONReporter().render(report)
229
+ else:
230
+ output = ConsoleReporter().render(report)
231
+
232
+ print(output)
233
+ return 0
234
+
235
+ except FileNotFoundError as exc:
236
+ missing_file = args.file
237
+
238
+ if args.test is not None and args.test in str(exc):
239
+ missing_file = args.test
240
+
241
+ elif (
242
+ args.validation is not None
243
+ and args.validation in str(exc)
244
+ ):
245
+ missing_file = args.validation
246
+
247
+ print(
248
+ f"Error: dataset file not found: {missing_file}",
249
+ file=sys.stderr,
250
+ )
251
+ return 1
252
+
253
+ except pd.errors.EmptyDataError as exc:
254
+ print(
255
+ f"Error: dataset file is empty: {exc}",
256
+ file=sys.stderr,
257
+ )
258
+ return 1
259
+
260
+ except pd.errors.ParserError as exc:
261
+ print(
262
+ f"Error: could not parse CSV file: {exc}",
263
+ file=sys.stderr,
264
+ )
265
+ return 1
266
+
267
+ except (TypeError, ValueError) as exc:
268
+ print(
269
+ f"Error: {exc}",
270
+ file=sys.stderr,
271
+ )
272
+ return 1
273
+
274
+
275
+ if __name__ == "__main__":
276
+ raise SystemExit(main())
File without changes
File without changes
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import pandas as pd
4
+
5
+ from dataleaks.engine.detector import BaseDetector
6
+ from dataleaks.schemas.dataset import DatasetContext
7
+ from dataleaks.schemas.finding import Finding
8
+
9
+
10
+ class CrossDatasetOverlapDetector(BaseDetector):
11
+ """Detect exact overlap between the current dataset and an external dataset."""
12
+
13
+ name = "cross_dataset_overlap"
14
+ category = "cross_dataset_leakage"
15
+
16
+ def __init__(
17
+ self,
18
+ reference_data: pd.DataFrame,
19
+ columns: list[str] | None = None,
20
+ ) -> None:
21
+ if not isinstance(reference_data, pd.DataFrame):
22
+ raise TypeError(
23
+ "reference_data must be a pandas DataFrame"
24
+ )
25
+
26
+ if columns is not None and not columns:
27
+ raise ValueError(
28
+ "columns must contain at least one column"
29
+ )
30
+
31
+ self.reference_data = reference_data
32
+ self.columns = columns
33
+
34
+ def detect(self, context: DatasetContext) -> list[Finding]:
35
+ data = context.data
36
+ reference = self.reference_data
37
+
38
+ if data.empty or reference.empty:
39
+ return []
40
+
41
+ columns = self._resolve_columns(
42
+ data,
43
+ reference,
44
+ )
45
+
46
+ if not columns:
47
+ return []
48
+
49
+ current_rows = (
50
+ data[columns]
51
+ .drop_duplicates()
52
+ .reset_index(drop=True)
53
+ )
54
+
55
+ reference_rows = (
56
+ reference[columns]
57
+ .drop_duplicates()
58
+ .reset_index(drop=True)
59
+ )
60
+
61
+ current_index = pd.MultiIndex.from_frame(current_rows)
62
+ reference_index = pd.MultiIndex.from_frame(reference_rows)
63
+
64
+ overlap = current_index.intersection(reference_index)
65
+ overlap_count = len(overlap)
66
+
67
+ if overlap_count == 0:
68
+ return []
69
+
70
+ current_unique_count = len(current_rows)
71
+
72
+ overlap_ratio = (
73
+ overlap_count / current_unique_count
74
+ if current_unique_count
75
+ else 0.0
76
+ )
77
+
78
+ if overlap_ratio >= 0.5:
79
+ severity = "critical"
80
+ elif overlap_ratio >= 0.1:
81
+ severity = "high"
82
+ else:
83
+ severity = "medium"
84
+
85
+ return [
86
+ Finding(
87
+ detector=self.name,
88
+ category=self.category,
89
+ severity=severity,
90
+ confidence=overlap_ratio,
91
+ explanation=(
92
+ f"{overlap_count} unique row(s) from the current "
93
+ "dataset also occur in the reference dataset."
94
+ ),
95
+ recommendation=(
96
+ "Verify whether the reference dataset was used during "
97
+ "training, preprocessing, augmentation, or evaluation. "
98
+ "Ensure that shared observations cannot transfer "
99
+ "target or evaluation information into the model."
100
+ ),
101
+ affected_columns=columns,
102
+ evidence={
103
+ "type": "cross_dataset_exact_overlap",
104
+ "overlap_count": overlap_count,
105
+ "current_unique_rows": current_unique_count,
106
+ "reference_unique_rows": len(reference_rows),
107
+ "overlap_ratio": overlap_ratio,
108
+ "columns": columns,
109
+ },
110
+ )
111
+ ]
112
+
113
+ def _resolve_columns(
114
+ self,
115
+ data: pd.DataFrame,
116
+ reference: pd.DataFrame,
117
+ ) -> list[str]:
118
+ if self.columns is not None:
119
+ missing = [
120
+ column
121
+ for column in self.columns
122
+ if column not in data.columns
123
+ or column not in reference.columns
124
+ ]
125
+
126
+ if missing:
127
+ raise ValueError(
128
+ f"Configured overlap columns are missing: {missing}"
129
+ )
130
+
131
+ return self.columns
132
+
133
+ return [
134
+ column
135
+ for column in data.columns
136
+ if column in reference.columns
137
+ ]
File without changes