dataeval 0.67.0__tar.gz → 0.68.0__tar.gz

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 (74) hide show
  1. {dataeval-0.67.0 → dataeval-0.68.0}/PKG-INFO +1 -1
  2. {dataeval-0.67.0 → dataeval-0.68.0}/pyproject.toml +2 -2
  3. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/__init__.py +1 -1
  4. dataeval-0.68.0/src/dataeval/_internal/detectors/duplicates.py +138 -0
  5. dataeval-0.68.0/src/dataeval/_internal/detectors/merged_stats.py +78 -0
  6. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/outliers.py +45 -17
  7. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/balance.py +42 -84
  8. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/coverage.py +11 -15
  9. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/diversity.py +23 -61
  10. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/stats.py +10 -0
  11. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/output.py +1 -1
  12. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/metrics/bias/__init__.py +2 -4
  13. dataeval-0.67.0/src/dataeval/_internal/detectors/duplicates.py +0 -109
  14. {dataeval-0.67.0 → dataeval-0.68.0}/LICENSE.txt +0 -0
  15. {dataeval-0.67.0 → dataeval-0.68.0}/README.md +0 -0
  16. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/__init__.py +0 -0
  17. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/clusterer.py +0 -0
  18. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/drift/__init__.py +0 -0
  19. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/drift/base.py +0 -0
  20. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/drift/cvm.py +0 -0
  21. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/drift/ks.py +0 -0
  22. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/drift/mmd.py +0 -0
  23. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/drift/torch.py +0 -0
  24. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/drift/uncertainty.py +0 -0
  25. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/ood/__init__.py +0 -0
  26. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/ood/ae.py +0 -0
  27. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/ood/aegmm.py +0 -0
  28. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/ood/base.py +0 -0
  29. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/ood/llr.py +0 -0
  30. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/ood/vae.py +0 -0
  31. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/detectors/ood/vaegmm.py +0 -0
  32. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/flags.py +0 -0
  33. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/interop.py +0 -0
  34. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/__init__.py +0 -0
  35. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/ber.py +0 -0
  36. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/divergence.py +0 -0
  37. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/parity.py +0 -0
  38. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/uap.py +0 -0
  39. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/metrics/utils.py +0 -0
  40. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/__init__.py +0 -0
  41. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/pytorch/__init__.py +0 -0
  42. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/pytorch/autoencoder.py +0 -0
  43. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/pytorch/blocks.py +0 -0
  44. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/pytorch/utils.py +0 -0
  45. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/tensorflow/__init__.py +0 -0
  46. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/tensorflow/autoencoder.py +0 -0
  47. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/tensorflow/gmm.py +0 -0
  48. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/tensorflow/losses.py +0 -0
  49. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/tensorflow/pixelcnn.py +0 -0
  50. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/tensorflow/trainer.py +0 -0
  51. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/models/tensorflow/utils.py +0 -0
  52. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/utils.py +0 -0
  53. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/workflows/__init__.py +0 -0
  54. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/_internal/workflows/sufficiency.py +0 -0
  55. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/detectors/__init__.py +0 -0
  56. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/detectors/drift/__init__.py +0 -0
  57. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/detectors/drift/kernels/__init__.py +0 -0
  58. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/detectors/drift/updates/__init__.py +0 -0
  59. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/detectors/linters/__init__.py +0 -0
  60. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/detectors/ood/__init__.py +0 -0
  61. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/flags/__init__.py +0 -0
  62. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/metrics/__init__.py +0 -0
  63. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/metrics/estimators/__init__.py +0 -0
  64. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/metrics/stats/__init__.py +0 -0
  65. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/py.typed +0 -0
  66. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/tensorflow/__init__.py +0 -0
  67. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/tensorflow/loss/__init__.py +0 -0
  68. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/tensorflow/models/__init__.py +0 -0
  69. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/tensorflow/recon/__init__.py +0 -0
  70. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/torch/__init__.py +0 -0
  71. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/torch/models/__init__.py +0 -0
  72. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/torch/trainer/__init__.py +0 -0
  73. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/utils/__init__.py +0 -0
  74. {dataeval-0.67.0 → dataeval-0.68.0}/src/dataeval/workflows/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dataeval
3
- Version: 0.67.0
3
+ Version: 0.68.0
4
4
  Summary: DataEval provides a simple interface to characterize image data and its impact on model performance across classification and object-detection tasks
5
5
  Home-page: https://dataeval.ai/
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dataeval"
3
- version = "0.67.0" # dynamic
3
+ version = "0.68.0" # dynamic
4
4
  description = "DataEval provides a simple interface to characterize image data and its impact on model performance across classification and object-detection tasks"
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -174,7 +174,7 @@ docstring-code-format = true
174
174
  docstring-code-line-length = "dynamic"
175
175
 
176
176
  [tool.codespell]
177
- skip = './*env*,./prototype,./docs/.jupyter_cache,./.tox,CHANGELOG.md,poetry.lock,./output,*.html'
177
+ skip = './*env*,./prototype,./output,./docs/_build,./docs/.jupyter_cache,CHANGELOG.md,poetry.lock,*.html'
178
178
 
179
179
  [build-system]
180
180
  requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
@@ -1,4 +1,4 @@
1
- __version__ = "0.67.0"
1
+ __version__ = "0.68.0"
2
2
 
3
3
  from importlib.util import find_spec
4
4
 
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Generic, Iterable, Sequence, TypeVar, cast
5
+
6
+ from numpy.typing import ArrayLike
7
+
8
+ from dataeval._internal.detectors.merged_stats import combine_stats, get_dataset_step_from_idx
9
+ from dataeval._internal.flags import ImageStat
10
+ from dataeval._internal.metrics.stats import StatsOutput, imagestats
11
+ from dataeval._internal.output import OutputMetadata, set_metadata
12
+
13
+ DuplicateGroup = list[int]
14
+ DatasetDuplicateGroupMap = dict[int, DuplicateGroup]
15
+ TIndexCollection = TypeVar("TIndexCollection", DuplicateGroup, DatasetDuplicateGroupMap)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DuplicatesOutput(Generic[TIndexCollection], OutputMetadata):
20
+ """
21
+ Attributes
22
+ ----------
23
+ exact : list[list[int] | dict[int, list[int]]]
24
+ Indices of images that are exact matches
25
+ near: list[list[int] | dict[int, list[int]]]
26
+ Indices of images that are near matches
27
+
28
+ - For a single dataset, indices are returned as a list of index groups.
29
+ - For multiple datasets, indices are returned as dictionaries where the key is the
30
+ index of the dataset, and the value is the list index groups from that dataset.
31
+ """
32
+
33
+ exact: list[TIndexCollection]
34
+ near: list[TIndexCollection]
35
+
36
+
37
+ class Duplicates:
38
+ """
39
+ Finds the duplicate images in a dataset using xxhash for exact duplicates
40
+ and pchash for near duplicates
41
+
42
+ Attributes
43
+ ----------
44
+ stats : StatsOutput
45
+ Output class of stats
46
+
47
+ Parameters
48
+ ----------
49
+ only_exact : bool, default False
50
+ Only inspect the dataset for exact image matches
51
+
52
+ Example
53
+ -------
54
+ Initialize the Duplicates class:
55
+
56
+ >>> dups = Duplicates()
57
+ """
58
+
59
+ def __init__(self, only_exact: bool = False):
60
+ self.stats: StatsOutput
61
+ self.only_exact = only_exact
62
+
63
+ def _get_duplicates(self) -> dict[str, list[list[int]]]:
64
+ stats_dict = self.stats.dict()
65
+ if "xxhash" in stats_dict:
66
+ exact_dict: dict[int, list] = {}
67
+ for i, value in enumerate(stats_dict["xxhash"]):
68
+ exact_dict.setdefault(value, []).append(i)
69
+ exact = [sorted(v) for v in exact_dict.values() if len(v) > 1]
70
+ else:
71
+ exact = []
72
+
73
+ if "pchash" in stats_dict and not self.only_exact:
74
+ near_dict: dict[int, list] = {}
75
+ for i, value in enumerate(stats_dict["pchash"]):
76
+ near_dict.setdefault(value, []).append(i)
77
+ near = [sorted(v) for v in near_dict.values() if len(v) > 1 and not any(set(v).issubset(x) for x in exact)]
78
+ else:
79
+ near = []
80
+
81
+ return {
82
+ "exact": sorted(exact),
83
+ "near": sorted(near),
84
+ }
85
+
86
+ @set_metadata("dataeval.detectors", ["only_exact"])
87
+ def evaluate(self, data: Iterable[ArrayLike] | StatsOutput | Sequence[StatsOutput]) -> DuplicatesOutput:
88
+ """
89
+ Returns duplicate image indices for both exact matches and near matches
90
+
91
+ Parameters
92
+ ----------
93
+ data : Iterable[ArrayLike], shape - (N, C, H, W) | StatsOutput | Sequence[StatsOutput]
94
+ A dataset of images in an ArrayLike format or the output(s) from an imagestats metric analysis
95
+
96
+ Returns
97
+ -------
98
+ DuplicatesOutput
99
+ List of groups of indices that are exact and near matches
100
+
101
+ See Also
102
+ --------
103
+ imagestats
104
+
105
+ Example
106
+ -------
107
+ >>> dups.evaluate(images)
108
+ DuplicatesOutput(exact=[[3, 20], [16, 37]], near=[[3, 20, 22], [12, 18], [13, 36], [14, 31], [17, 27], [19, 38, 47]])
109
+ """ # noqa: E501
110
+
111
+ stats, dataset_steps = combine_stats(data)
112
+
113
+ if isinstance(stats, StatsOutput):
114
+ if not stats.xxhash:
115
+ raise ValueError("StatsOutput must include xxhash information of the images.")
116
+ if not self.only_exact and not stats.pchash:
117
+ raise ValueError("StatsOutput must include pchash information of the images for near matches.")
118
+ self.stats = stats
119
+ else:
120
+ flags = ImageStat.XXHASH | (ImageStat(0) if self.only_exact else ImageStat.PCHASH)
121
+ self.stats = imagestats(cast(Iterable[ArrayLike], data), flags)
122
+
123
+ duplicates = self._get_duplicates()
124
+
125
+ # split up results from combined dataset into individual dataset buckets
126
+ if dataset_steps:
127
+ dup_list: list[list[int]]
128
+ for dup_type, dup_list in duplicates.items():
129
+ dup_list_dict = []
130
+ for idxs in dup_list:
131
+ dup_dict = {}
132
+ for idx in idxs:
133
+ k, v = get_dataset_step_from_idx(idx, dataset_steps)
134
+ dup_dict.setdefault(k, []).append(v)
135
+ dup_list_dict.append(dup_dict)
136
+ duplicates[dup_type] = dup_list_dict
137
+
138
+ return DuplicatesOutput(**duplicates)
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Sequence, cast
4
+ from warnings import warn
5
+
6
+ import numpy as np
7
+
8
+ from dataeval._internal.metrics.stats import StatsOutput
9
+ from dataeval._internal.output import populate_defaults
10
+
11
+
12
+ def add_stats(a: StatsOutput, b: StatsOutput) -> StatsOutput:
13
+ if not isinstance(a, StatsOutput) or not isinstance(b, StatsOutput):
14
+ raise TypeError(f"Cannot add object of type {type(a)} and type {type(b)}.")
15
+
16
+ a_dict = a.dict()
17
+ b_dict = b.dict()
18
+ a_keys = set(a_dict)
19
+ b_keys = set(b_dict)
20
+
21
+ missing_keys = a_keys - b_keys
22
+ if missing_keys:
23
+ raise ValueError(f"Required keys are missing: {missing_keys}.")
24
+
25
+ extra_keys = b_keys - a_keys
26
+ if extra_keys:
27
+ warn(f"Extraneous keys will be dropped: {extra_keys}.")
28
+
29
+ # perform add of multi-channel stats
30
+ if "ch_idx_map" in a_dict:
31
+ for k, v in a_dict.items():
32
+ if k == "ch_idx_map":
33
+ offset = sum([len(idxs) for idxs in v.values()])
34
+ for ch_k, ch_v in b_dict[k].items():
35
+ if ch_k not in v:
36
+ v[ch_k] = []
37
+ a_dict[k][ch_k].extend([idx + offset for idx in ch_v])
38
+ else:
39
+ for ch_k in b_dict[k]:
40
+ if ch_k not in v:
41
+ v[ch_k] = b_dict[k][ch_k]
42
+ else:
43
+ v[ch_k] = np.concatenate((v[ch_k], b_dict[k][ch_k]), axis=1)
44
+ else:
45
+ for k in a_dict:
46
+ if isinstance(a_dict[k], list):
47
+ a_dict[k].extend(b_dict[k])
48
+ else:
49
+ a_dict[k] = np.concatenate((a_dict[k], b_dict[k]))
50
+
51
+ return StatsOutput(**populate_defaults(a_dict, StatsOutput))
52
+
53
+
54
+ def combine_stats(stats) -> tuple[StatsOutput | None, list[int]]:
55
+ dataset_steps = []
56
+
57
+ if isinstance(stats, StatsOutput):
58
+ return stats, dataset_steps
59
+
60
+ output = None
61
+ if isinstance(stats, Sequence) and isinstance(stats[0], StatsOutput):
62
+ stats = cast(Sequence[StatsOutput], stats)
63
+ cur_len = 0
64
+ for s in stats:
65
+ output = s if output is None else add_stats(output, s)
66
+ cur_len += len(s)
67
+ dataset_steps.append(cur_len)
68
+
69
+ return output, dataset_steps
70
+
71
+
72
+ def get_dataset_step_from_idx(idx: int, dataset_steps: list[int]) -> tuple[int, int]:
73
+ last_step = 0
74
+ for i, step in enumerate(dataset_steps):
75
+ if idx < step:
76
+ return i, idx - last_step
77
+ last_step = step
78
+ return -1, idx
@@ -1,27 +1,39 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Iterable, Literal
4
+ from typing import Iterable, Literal, Sequence, cast
5
+ from warnings import warn
5
6
 
6
7
  import numpy as np
7
8
  from numpy.typing import ArrayLike, NDArray
8
9
 
10
+ from dataeval._internal.detectors.merged_stats import combine_stats, get_dataset_step_from_idx
9
11
  from dataeval._internal.flags import ImageStat, to_distinct, verify_supported
10
12
  from dataeval._internal.metrics.stats import StatsOutput, imagestats
11
13
  from dataeval._internal.output import OutputMetadata, set_metadata
12
14
 
15
+ IndexIssueMap = dict[int, dict[str, float]]
16
+ DatasetIndexIssueMap = dict[int, IndexIssueMap]
17
+ """
18
+ Mapping of image indices to a dictionary of issue types and calculated values
19
+ """
20
+
13
21
 
14
22
  @dataclass(frozen=True)
15
23
  class OutliersOutput(OutputMetadata):
16
24
  """
17
25
  Attributes
18
26
  ----------
19
- issues : Dict[int, Dict[str, float]]
20
- Dictionary containing the indices of outliers and a dictionary showing
21
- the issues and calculated values for the given index.
27
+ issues : dict[int, dict[str, float]] | dict[int, dict[int, dict[str, float]]]
28
+ Indices of image outliers with their associated issue type and calculated values.
29
+
30
+ - For a single dataset, a dictionary containing the indices of outliers and
31
+ a dictionary showing the issues and calculated values for the given index.
32
+ - For multiple datasets, a map of dataset indices to the indices of outliers
33
+ and their associated issues and calculated values.
22
34
  """
23
35
 
24
- issues: dict[int, dict[str, float]]
36
+ issues: IndexIssueMap | DatasetIndexIssueMap
25
37
 
26
38
 
27
39
  def _get_outlier_mask(
@@ -64,7 +76,7 @@ class Outliers:
64
76
 
65
77
  Attributes
66
78
  ----------
67
- stats : Dict[str, Any]
79
+ stats : dict[str, Any]
68
80
  Dictionary to hold the value of each metric for each image
69
81
 
70
82
  See Also
@@ -135,14 +147,14 @@ class Outliers:
135
147
  return dict(sorted(flagged_images.items()))
136
148
 
137
149
  @set_metadata("dataeval.detectors", ["flags", "outlier_method", "outlier_threshold"])
138
- def evaluate(self, data: Iterable[ArrayLike] | StatsOutput) -> OutliersOutput:
150
+ def evaluate(self, data: Iterable[ArrayLike] | StatsOutput | Sequence[StatsOutput]) -> OutliersOutput:
139
151
  """
140
152
  Returns indices of outliers with the issues identified for each
141
153
 
142
154
  Parameters
143
155
  ----------
144
- data : Iterable[ArrayLike], shape - (C, H, W) | StatsOutput
145
- A dataset of images in an ArrayLike format or the output from an imagestats metric analysis
156
+ data : Iterable[ArrayLike], shape - (C, H, W) | StatsOutput | Sequence[StatsOutput]
157
+ A dataset of images in an ArrayLike format or the output(s) from an imagestats metric analysis
146
158
 
147
159
  Returns
148
160
  -------
@@ -157,13 +169,29 @@ class Outliers:
157
169
  >>> outliers.evaluate(images)
158
170
  OutliersOutput(issues={18: {'brightness': 0.78}, 25: {'brightness': 0.98}})
159
171
  """
160
- if isinstance(data, StatsOutput):
161
- flags = set(to_distinct(self.flags).values())
162
- stats = set(data.dict())
163
- missing = flags - stats
172
+ stats, dataset_steps = combine_stats(data)
173
+
174
+ if isinstance(stats, StatsOutput):
175
+ selected_flags = set(to_distinct(self.flags).values())
176
+ provided = set(stats.dict())
177
+ missing = selected_flags - provided
164
178
  if missing:
165
- raise ValueError(f"StatsOutput is missing {missing} from the required stats: {flags}.")
166
- self.stats = data
179
+ warn(
180
+ f"StatsOutput provided {provided} and is missing {missing} \
181
+ from the selected stat flags: {selected_flags}."
182
+ )
183
+ self.stats = stats
167
184
  else:
168
- self.stats = imagestats(data, self.flags)
169
- return OutliersOutput(self._get_outliers())
185
+ self.stats = imagestats(cast(Iterable[ArrayLike], data), self.flags)
186
+
187
+ outliers = self._get_outliers()
188
+
189
+ # split up results from combined dataset into individual dataset buckets
190
+ if dataset_steps:
191
+ out_dict = {}
192
+ for idx, issue in outliers.items():
193
+ k, v = get_dataset_step_from_idx(idx, dataset_steps)
194
+ out_dict.setdefault(k, {})[v] = issue
195
+ outliers = out_dict
196
+
197
+ return OutliersOutput(outliers)
@@ -17,11 +17,17 @@ class BalanceOutput(OutputMetadata):
17
17
  """
18
18
  Attributes
19
19
  ----------
20
- mutual_information : NDArray[np.float64]
20
+ balance : NDArray[np.float64]
21
21
  Estimate of mutual information between metadata factors and class label
22
+ factors : NDArray[np.float64]
23
+ Estimate of inter/intra-factor mutual information
24
+ classwise : NDArray[np.float64]
25
+ Estimate of mutual information between metadata factors and individual class labels
22
26
  """
23
27
 
24
- mutual_information: NDArray[np.float64]
28
+ balance: NDArray[np.float64]
29
+ factors: NDArray[np.float64]
30
+ classwise: NDArray[np.float64]
25
31
 
26
32
 
27
33
  def validate_num_neighbors(num_neighbors: int) -> int:
@@ -77,17 +83,22 @@ def balance(class_labels: Sequence[int], metadata: list[dict], num_neighbors: in
77
83
  -------
78
84
  Return balance (mutual information) of factors with class_labels
79
85
 
80
- >>> balance(class_labels, metadata).mutual_information[0]
81
- array([0.99999822, 0.13363788, 0. , 0.02994455])
86
+ >>> bal = balance(class_labels, metadata)
87
+ >>> bal.balance
88
+ array([0.99999822, 0.13363788, 0.04505382, 0.02994455])
82
89
 
83
- Return balance (mutual information) of metadata factors with class_labels
84
- and each other
90
+ Return intra/interfactor balance (mutual information)
85
91
 
86
- >>> balance(class_labels, metadata).mutual_information
87
- array([[0.99999822, 0.13363788, 0. , 0.02994455],
88
- [0.13363788, 0.99999843, 0.01389763, 0.09725766],
89
- [0. , 0.01389763, 0.48549233, 0.15314612],
90
- [0.02994455, 0.09725766, 0.15314612, 0.99999856]])
92
+ >>> bal.factors
93
+ array([[0.99999843, 0.03510422, 0.09725766],
94
+ [0.03510422, 0.08433558, 0.15621459],
95
+ [0.09725766, 0.15621459, 0.99999856]])
96
+
97
+ Return classwise balance (mutual information) of factors with individual class_labels
98
+
99
+ >>> bal.classwise
100
+ array([[0.99999822, 0.13363788, 0. , 0. ],
101
+ [0.99999822, 0.13363788, 0. , 0. ]])
91
102
 
92
103
  See Also
93
104
  --------
@@ -102,13 +113,9 @@ def balance(class_labels: Sequence[int], metadata: list[dict], num_neighbors: in
102
113
  mi[:] = np.nan
103
114
 
104
115
  for idx in range(num_factors):
105
- tgt = data[:, idx]
116
+ tgt = data[:, idx].astype(int)
106
117
 
107
118
  if is_categorical[idx]:
108
- if tgt.dtype == float:
109
- # map to unique integers if categorical
110
- _, tgt = np.unique(tgt, return_inverse=True)
111
- # categorical target
112
119
  mi[idx, :] = mutual_info_classif(
113
120
  data,
114
121
  tgt,
@@ -129,89 +136,40 @@ def balance(class_labels: Sequence[int], metadata: list[dict], num_neighbors: in
129
136
  norm_factor = 0.5 * np.add.outer(ent_all, ent_all) + 1e-6
130
137
  # in principle MI should be symmetric, but it is not in practice.
131
138
  nmi = 0.5 * (mi + mi.T) / norm_factor
139
+ balance = nmi[0]
140
+ factors = nmi[1:, 1:]
132
141
 
133
- return BalanceOutput(nmi)
134
-
135
-
136
- @set_metadata("dataeval.metrics")
137
- def balance_classwise(class_labels: Sequence[int], metadata: list[dict], num_neighbors: int = 5) -> BalanceOutput:
138
- """
139
- Compute mutual information (analogous to correlation) between metadata factors
140
- (class label, metadata, label/image properties) with individual class labels.
141
-
142
- Parameters
143
- ----------
144
- class_labels: Sequence[int]
145
- List of class labels for each image
146
- metadata: List[Dict]
147
- List of metadata factors for each image
148
- num_neighbors: int, default 5
149
- Number of nearest neighbors to use for computing MI between discrete
150
- and continuous variables.
151
-
152
- Notes
153
- -----
154
- We use `mutual_info_classif` from sklearn since class label is categorical.
155
- `mutual_info_classif` outputs are consistent up to O(1e-4) and depend on a random
156
- seed. MI is computed differently for categorical and continuous variables, so we
157
- have to specify with is_categorical.
158
-
159
- Returns
160
- -------
161
- BalanceOutput
162
- (num_classes x num_factors) estimate of mutual information between
163
- num_factors metadata factors and individual class labels.
164
-
165
- Example
166
- -------
167
- Return classwise balance (mutual information) of factors with individual class_labels
168
-
169
- >>> balance_classwise(class_labels, metadata).mutual_information
170
- array([[0.13363788, 0.54085156, 0. ],
171
- [0.13363788, 0.54085156, 0. ]])
172
-
173
-
174
- See Also
175
- --------
176
- sklearn.feature_selection.mutual_info_classif
177
- sklearn.feature_selection.mutual_info_regression
178
- sklearn.metrics.mutual_info_score
179
- compute_mutual_information
180
- """
181
- num_neighbors = validate_num_neighbors(num_neighbors)
182
- data, names, is_categorical = preprocess_metadata(class_labels, metadata)
183
- num_factors = len(names)
184
142
  # unique class labels
185
143
  class_idx = names.index("class_label")
186
- class_data = data[:, class_idx]
144
+ class_data = data[:, class_idx].astype(int)
187
145
  u_cls = np.unique(class_data)
188
146
  num_classes = len(u_cls)
189
147
 
190
- data_no_class = np.concatenate((data[:, :class_idx], data[:, (class_idx + 1) :]), axis=1)
191
-
192
148
  # assume class is a factor
193
- mi = np.empty((num_classes, num_factors - 1))
194
- mi[:] = np.nan
149
+ classwise_mi = np.empty((num_classes, num_factors))
150
+ classwise_mi[:] = np.nan
195
151
 
196
152
  # categorical variables, excluding class label
197
153
  cat_mask = np.concatenate((is_categorical[:class_idx], is_categorical[(class_idx + 1) :]), axis=0).astype(int)
198
154
 
155
+ tgt_bin = np.stack([class_data == cls for cls in u_cls]).T.astype(int)
156
+ ent_tgt_bin = entropy(
157
+ tgt_bin, names=[str(idx) for idx in range(num_classes)], is_categorical=[True for idx in range(num_classes)]
158
+ )
159
+
199
160
  # classification MI for discrete/categorical features
200
- for idx, cls in enumerate(u_cls):
201
- tgt = class_data == cls
161
+ for idx in range(num_classes):
162
+ # tgt = class_data == cls
202
163
  # units: nat
203
- mi[idx, :] = mutual_info_classif(
204
- data_no_class,
205
- tgt,
164
+ classwise_mi[idx, :] = mutual_info_classif(
165
+ data,
166
+ tgt_bin[:, idx],
206
167
  discrete_features=cat_mask, # type: ignore
207
168
  n_neighbors=num_neighbors,
208
169
  random_state=0,
209
170
  )
210
171
 
211
- # let this recompute for all features including class label
212
- ent_all = entropy(data, names, is_categorical)
213
- ent_tgt = ent_all[class_idx]
214
- ent_all = np.concatenate((ent_all[:class_idx], ent_all[(class_idx + 1) :]), axis=0)
215
- norm_factor = 0.5 * np.add.outer(ent_tgt, ent_all) + 1e-6
216
- nmi = mi / norm_factor
217
- return BalanceOutput(nmi)
172
+ norm_factor = 0.5 * np.add.outer(ent_tgt_bin, ent_all) + 1e-6
173
+ classwise = classwise_mi / norm_factor
174
+
175
+ return BalanceOutput(balance, factors, classwise)
@@ -66,27 +66,22 @@ def coverage(
66
66
 
67
67
  Note
68
68
  ----
69
- Embeddings should be on the unit interval.
69
+ Embeddings should be on the unit interval [0-1].
70
70
 
71
71
  Example
72
72
  -------
73
- >>> coverage(embeddings)
74
- CoverageOutput(indices=array([], dtype=int64), radii=array([0.59307666, 0.56956307, 0.56328616, 0.70660265, 0.57778087,
75
- 0.53738624, 0.58968217, 1.27721334, 0.84378694, 0.67767021,
76
- 0.69680335, 1.35532621, 0.59764166, 0.8691945 , 0.83627602,
77
- 0.84187303, 0.62212358, 1.09039732, 0.67956797, 0.60134383,
78
- 0.83713908, 0.91784263, 1.12901193, 0.73907618, 0.63943983,
79
- 0.61188447, 0.47872713, 0.57207771, 0.92885883, 0.54750511,
80
- 0.83015726, 1.20721778, 0.50421928, 0.98312246, 0.59764166,
81
- 0.61009202, 0.73864073, 1.0381061 , 0.77598609, 0.72984036,
82
- 0.67573006, 0.48056064, 1.00050879, 0.89532971, 0.58395529,
83
- 0.95954793, 0.60134383, 1.10096454, 0.51955314, 0.73038702]), critical_value=0)
73
+ >>> results = coverage(embeddings)
74
+ >>> results.indices
75
+ array([447, 412, 8, 32, 63])
76
+ >>> results.critical_value
77
+ 0.8459038956941765
84
78
 
85
79
  Reference
86
80
  ---------
87
81
  This implementation is based on https://dl.acm.org/doi/abs/10.1145/3448016.3457315.
82
+
88
83
  [1] Seymour Sudman. 1976. Applied sampling. Academic Press New York (1976).
89
- """ # noqa: E501
84
+ """
90
85
 
91
86
  # Calculate distance matrix, look at the (k+1)th farthest neighbor for each image.
92
87
  embeddings = to_numpy(embeddings)
@@ -105,8 +100,9 @@ def coverage(
105
100
  pvals = np.where(crit > rho)[0]
106
101
  elif radius_type == "adaptive":
107
102
  # Use data adaptive cutoff as rho
108
- rho = int(n * percent)
109
- pvals = np.argsort(crit)[::-1][:rho]
103
+ selection = int(max(n * percent, 1))
104
+ pvals = np.argsort(crit)[::-1][:selection]
105
+ rho = float(np.mean(np.sort(crit)[::-1][selection - 1 : selection + 1]))
110
106
  else:
111
107
  raise ValueError(f"{radius_type} is an invalid radius type. Expected 'adaptive' or 'naive'")
112
108
  return CoverageOutput(pvals, crit, rho)
@@ -17,9 +17,12 @@ class DiversityOutput(OutputMetadata):
17
17
  ----------
18
18
  diversity_index : NDArray[np.float64]
19
19
  Diversity index for classes and factors
20
+ classwise : NDArray[np.float64]
21
+ Classwise diversity index [n_class x n_factor]
20
22
  """
21
23
 
22
24
  diversity_index: NDArray[np.float64]
25
+ classwise: NDArray[np.float64]
23
26
 
24
27
 
25
28
  def diversity_shannon(
@@ -139,9 +142,11 @@ def diversity(
139
142
  class_labels: Sequence[int], metadata: list[dict], method: Literal["shannon", "simpson"] = "simpson"
140
143
  ) -> DiversityOutput:
141
144
  """
142
- Compute diversity for discrete/categorical variables and, through standard
145
+ Compute diversity and classwise diversity for discrete/categorical variables and, through standard
143
146
  histogram binning, for continuous variables.
144
147
 
148
+ We define diversity as a normalized form of the inverse Simpson diversity index.
149
+
145
150
  diversity = 1 implies that samples are evenly distributed across a particular factor
146
151
  diversity = 0 implies that all samples belong to one category/bin
147
152
 
@@ -157,89 +162,45 @@ def diversity(
157
162
  Notes
158
163
  -----
159
164
  - For continuous variables, histogram bins are chosen automatically. See numpy.histogram for details.
165
+ - The expression is undefined for q=1, but it approaches the Shannon entropy in the limit.
166
+ - If there is only one category, the diversity index takes a value of 1 = 1/N = 1/1. Entropy will take a value of 0.
160
167
 
161
168
  Returns
162
169
  -------
163
170
  DiversityOutput
164
- Diversity index per column of self.data or each factor in self.names
171
+ Diversity index per column of self.data or each factor in self.names and
172
+ classwise diversity [n_class x n_factor]
165
173
 
166
174
  Example
167
175
  -------
168
176
  Compute Simpson diversity index of metadata and class labels
169
177
 
170
- >>> diversity(class_labels, metadata, method="simpson").diversity_index
178
+ >>> div_simp = diversity(class_labels, metadata, method="simpson")
179
+ >>> div_simp.diversity_index
171
180
  array([0.18103448, 0.18103448, 0.88636364])
172
181
 
173
- Compute Shannon diversity index of metadata and class labels
174
-
175
- >>> diversity(class_labels, metadata, method="shannon").diversity_index
176
- array([0.37955133, 0.37955133, 0.96748876])
177
-
178
-
179
- See Also
180
- --------
181
- numpy.histogram
182
- """
183
- diversity_fn = get_method(DIVERSITY_FN_MAP, method)
184
- data, names, is_categorical = preprocess_metadata(class_labels, metadata)
185
- diversity_index = diversity_fn(data, names, is_categorical, None).astype(np.float64)
186
- return DiversityOutput(diversity_index)
187
-
188
-
189
- @set_metadata("dataeval.metrics")
190
- def diversity_classwise(
191
- class_labels: Sequence[int], metadata: list[dict], method: Literal["shannon", "simpson"] = "simpson"
192
- ) -> DiversityOutput:
193
- """
194
- Compute diversity for discrete/categorical variables and, through standard
195
- histogram binning, for continuous variables.
196
-
197
- We define diversity as a normalized form of the inverse Simpson diversity
198
- index.
199
-
200
- diversity = 1 implies that samples are evenly distributed across a particular factor
201
- diversity = 0 implies that all samples belong to one category/bin
202
-
203
- Parameters
204
- ----------
205
- class_labels: Sequence[int]
206
- List of class labels for each image
207
- metadata: List[Dict]
208
- List of metadata factors for each image
209
- method: Literal["shannon", "simpson"], default "simpson"
210
- Indicates which diversity index should be computed
211
-
212
- Notes
213
- -----
214
- - For continuous variables, histogram bins are chosen automatically. See numpy.histogram for details.
215
- - If there is only one category, the diversity index takes a value of 0.
216
-
217
- Returns
218
- -------
219
- DiversityOutput
220
- Diversity index [n_class x n_factor]
221
-
222
- Example
223
- -------
224
- Compute classwise Simpson diversity index of metadata and class labels
225
-
226
- >>> diversity_classwise(class_labels, metadata, method="simpson").diversity_index
182
+ >>> div_simp.classwise
227
183
  array([[0.17241379, 0.39473684],
228
184
  [0.2 , 0.2 ]])
229
185
 
230
- Compute classwise Shannon diversity index of metadata and class labels
186
+ Compute Shannon diversity index of metadata and class labels
231
187
 
232
- >>> diversity_classwise(class_labels, metadata, method="shannon").diversity_index
188
+ >>> div_shan = diversity(class_labels, metadata, method="shannon")
189
+ >>> div_shan.diversity_index
190
+ array([0.37955133, 0.37955133, 0.96748876])
191
+
192
+ >>> div_shan.classwise
233
193
  array([[0.43156028, 0.83224889],
234
194
  [0.57938016, 0.57938016]])
235
195
 
236
-
237
196
  See Also
238
197
  --------
239
198
  numpy.histogram
240
199
  """
241
200
  diversity_fn = get_method(DIVERSITY_FN_MAP, method)
242
201
  data, names, is_categorical = preprocess_metadata(class_labels, metadata)
202
+ diversity_index = diversity_fn(data, names, is_categorical, None).astype(np.float64)
203
+
243
204
  class_idx = names.index("class_label")
244
205
  class_lbl = data[:, class_idx]
245
206
 
@@ -251,4 +212,5 @@ def diversity_classwise(
251
212
  subset_mask = class_lbl == cls
252
213
  diversity[idx, :] = diversity_fn(data, names, is_categorical, subset_mask)
253
214
  div_no_class = np.concatenate((diversity[:, :class_idx], diversity[:, (class_idx + 1) :]), axis=1)
254
- return DiversityOutput(div_no_class)
215
+
216
+ return DiversityOutput(diversity_index, div_no_class)
@@ -89,6 +89,16 @@ class StatsOutput(OutputMetadata):
89
89
  def dict(self):
90
90
  return {k: v for k, v in self.__dict__.items() if not k.startswith("_") and len(v) > 0}
91
91
 
92
+ def __len__(self) -> int:
93
+ if self.ch_idx_map:
94
+ return sum([len(idxs) for idxs in self.ch_idx_map.values()])
95
+ else:
96
+ for a in self.__annotations__:
97
+ attr = getattr(self, a, None)
98
+ if attr is not None and hasattr(a, "__len__") and len(attr) > 0:
99
+ return len(attr)
100
+ return 0
101
+
92
102
 
93
103
  QUARTILES = (0, 25, 50, 75, 100)
94
104
 
@@ -11,7 +11,7 @@ from dataeval import __version__
11
11
 
12
12
  class OutputMetadata:
13
13
  _name: str
14
- _execution_time: str
14
+ _execution_time: datetime
15
15
  _execution_duration: float
16
16
  _arguments: dict[str, str]
17
17
  _state: dict[str, str]
@@ -1,14 +1,12 @@
1
- from dataeval._internal.metrics.balance import balance, balance_classwise
1
+ from dataeval._internal.metrics.balance import balance
2
2
  from dataeval._internal.metrics.coverage import coverage
3
- from dataeval._internal.metrics.diversity import diversity, diversity_classwise
3
+ from dataeval._internal.metrics.diversity import diversity
4
4
  from dataeval._internal.metrics.parity import label_parity, parity
5
5
 
6
6
  __all__ = [
7
7
  "balance",
8
- "balance_classwise",
9
8
  "coverage",
10
9
  "diversity",
11
- "diversity_classwise",
12
10
  "label_parity",
13
11
  "parity",
14
12
  ]
@@ -1,109 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Iterable
5
-
6
- from numpy.typing import ArrayLike
7
-
8
- from dataeval._internal.flags import ImageStat
9
- from dataeval._internal.metrics.stats import StatsOutput, imagestats
10
- from dataeval._internal.output import OutputMetadata, set_metadata
11
-
12
-
13
- @dataclass(frozen=True)
14
- class DuplicatesOutput(OutputMetadata):
15
- """
16
- Attributes
17
- ----------
18
- exact : List[List[int]]
19
- Indices of images that are exact matches
20
- near: List[List[int]]
21
- Indices of images that are near matches
22
- """
23
-
24
- exact: list[list[int]]
25
- near: list[list[int]]
26
-
27
-
28
- class Duplicates:
29
- """
30
- Finds the duplicate images in a dataset using xxhash for exact duplicates
31
- and pchash for near duplicates
32
-
33
- Attributes
34
- ----------
35
- stats : StatsOutput
36
- Output class of stats
37
-
38
- Parameters
39
- ----------
40
- only_exact : bool, default False
41
- Only inspect the dataset for exact image matches
42
-
43
- Example
44
- -------
45
- Initialize the Duplicates class:
46
-
47
- >>> dups = Duplicates()
48
- """
49
-
50
- def __init__(self, only_exact: bool = False):
51
- self.stats: StatsOutput
52
- self.only_exact = only_exact
53
-
54
- def _get_duplicates(self) -> dict[str, list[list[int]]]:
55
- stats_dict = self.stats.dict()
56
- if "xxhash" in stats_dict:
57
- exact = {}
58
- for i, value in enumerate(stats_dict["xxhash"]):
59
- exact.setdefault(value, []).append(i)
60
- exact = [v for v in exact.values() if len(v) > 1]
61
- else:
62
- exact = []
63
-
64
- if "pchash" in stats_dict and not self.only_exact:
65
- near = {}
66
- for i, value in enumerate(stats_dict["pchash"]):
67
- near.setdefault(value, []).append(i)
68
- near = [v for v in near.values() if len(v) > 1 and not any(set(v).issubset(x) for x in exact)]
69
- else:
70
- near = []
71
-
72
- return {
73
- "exact": sorted(exact),
74
- "near": sorted(near),
75
- }
76
-
77
- @set_metadata("dataeval.detectors", ["only_exact"])
78
- def evaluate(self, data: Iterable[ArrayLike] | StatsOutput) -> DuplicatesOutput:
79
- """
80
- Returns duplicate image indices for both exact matches and near matches
81
-
82
- Parameters
83
- ----------
84
- data : Iterable[ArrayLike], shape - (N, C, H, W) | StatsOutput
85
- A dataset of images in an ArrayLike format or the output from an imagestats metric analysis
86
-
87
- Returns
88
- -------
89
- DuplicatesOutput
90
- List of groups of indices that are exact and near matches
91
-
92
- See Also
93
- --------
94
- imagestats
95
-
96
- Example
97
- -------
98
- >>> dups.evaluate(images)
99
- DuplicatesOutput(exact=[[3, 20], [16, 37]], near=[[3, 20, 22], [12, 18], [13, 36], [14, 31], [17, 27], [19, 38, 47]])
100
- """ # noqa: E501
101
- if isinstance(data, StatsOutput):
102
- if not data.xxhash:
103
- raise ValueError("StatsOutput must include xxhash information of the images.")
104
- if not self.only_exact and not data.pchash:
105
- raise ValueError("StatsOutput must include pchash information of the images for near matches.")
106
- self.stats = data
107
- else:
108
- self.stats = imagestats(data, ImageStat.XXHASH | (ImageStat(0) if self.only_exact else ImageStat.PCHASH))
109
- return DuplicatesOutput(**self._get_duplicates())
File without changes
File without changes