osmotic-data-filter 0.1.8__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.
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Osmotic Data Filter (ODF) - v0.1.8 Bulletproof Edition
|
|
3
|
+
======================================================
|
|
4
|
+
A physical-chemistry inspired, safe, and hybrid outlier handling framework for Python.
|
|
5
|
+
Combines Interquartile Range (IQR) for critical boundaries with osmotic distance
|
|
6
|
+
adjustments for smooth value scaling without involuntary data deletion.
|
|
7
|
+
|
|
8
|
+
Author: Jana Elkasrawy
|
|
9
|
+
License: MIT
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
import seaborn as sns
|
|
16
|
+
|
|
17
|
+
def detect_outliers_preview(df, columns=None, iqr_factor=1.5):
|
|
18
|
+
if df is None or df.empty:
|
|
19
|
+
return pd.DataFrame()
|
|
20
|
+
if columns is None:
|
|
21
|
+
columns = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col])]
|
|
22
|
+
report = []
|
|
23
|
+
for col in columns:
|
|
24
|
+
if col not in df.columns or not pd.api.types.is_numeric_dtype(df[col]):
|
|
25
|
+
continue
|
|
26
|
+
series = df[col].replace([np.inf, -np.inf], np.nan).dropna()
|
|
27
|
+
if series.empty:
|
|
28
|
+
continue
|
|
29
|
+
q1 = series.quantile(0.25)
|
|
30
|
+
q3 = series.quantile(0.75)
|
|
31
|
+
iqr = q3 - q1
|
|
32
|
+
if pd.isna(iqr) or iqr <= 0:
|
|
33
|
+
iqr = 0.0
|
|
34
|
+
lower_limit = q1 - (iqr_factor * iqr)
|
|
35
|
+
upper_limit = q3 + (iqr_factor * iqr)
|
|
36
|
+
outliers_count = ((series < lower_limit) | (series > upper_limit)).sum()
|
|
37
|
+
total_len = len(series)
|
|
38
|
+
outlier_pct = (outliers_count / total_len * 100) if total_len > 0 else 0.0
|
|
39
|
+
report.append({
|
|
40
|
+
'Column': col,
|
|
41
|
+
'Q1': round(float(q1), 4),
|
|
42
|
+
'Median': round(float(series.median()), 4),
|
|
43
|
+
'Q3': round(float(q3), 4),
|
|
44
|
+
'IQR': round(float(iqr), 4),
|
|
45
|
+
'Lower_Critical_Limit': round(float(lower_limit), 4),
|
|
46
|
+
'Upper_Critical_Limit': round(float(upper_limit), 4),
|
|
47
|
+
'Outliers_Count': int(outliers_count),
|
|
48
|
+
'Outlier_Percentage (%)': round(float(outlier_pct), 2)
|
|
49
|
+
})
|
|
50
|
+
return pd.DataFrame(report)
|
|
51
|
+
|
|
52
|
+
def partition_uncertain_cases(df, target_column, iqr_factor=1.5):
|
|
53
|
+
if df is None or df.empty or target_column not in df.columns:
|
|
54
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
55
|
+
series = df[target_column].replace([np.inf, -np.inf], np.nan).dropna()
|
|
56
|
+
if series.empty:
|
|
57
|
+
return df.copy(), pd.DataFrame()
|
|
58
|
+
q1 = series.quantile(0.25)
|
|
59
|
+
q3 = series.quantile(0.75)
|
|
60
|
+
iqr = q3 - q1
|
|
61
|
+
if pd.isna(iqr) or iqr <= 0:
|
|
62
|
+
iqr = 0.0
|
|
63
|
+
lower_limit = q1 - (iqr_factor * iqr)
|
|
64
|
+
upper_limit = q3 + (iqr_factor * iqr)
|
|
65
|
+
temp_series = df[target_column].replace([np.inf, -np.inf], np.nan)
|
|
66
|
+
outlier_mask = (temp_series < lower_limit) | (temp_series > upper_limit)
|
|
67
|
+
outlier_mask = outlier_mask.fillna(False)
|
|
68
|
+
return df[~outlier_mask].copy(), df[outlier_mask].copy()
|
|
69
|
+
|
|
70
|
+
def Osmotic_Data_FLTR(df, columns=None, iqr_factor=1.5, osmotic_scale=0.5, action='adjust', round_to_integer=False):
|
|
71
|
+
if df is None:
|
|
72
|
+
return pd.DataFrame()
|
|
73
|
+
if action not in ['adjust', 'flag']:
|
|
74
|
+
raise ValueError("Action must be either 'adjust' or 'flag'")
|
|
75
|
+
|
|
76
|
+
if osmotic_scale not in ['adj_auto', 'adj_dnmc']:
|
|
77
|
+
if not isinstance(osmotic_scale, (int, float)) or pd.isna(osmotic_scale):
|
|
78
|
+
raise ValueError("Osmotic scale must be 'adj_auto', 'adj_dnmc', or a numeric value.")
|
|
79
|
+
if not (0.0 < osmotic_scale < 1.0):
|
|
80
|
+
raise ValueError("Numeric osmotic scale must be strictly greater than 0 and less than 1.")
|
|
81
|
+
|
|
82
|
+
df_result = df.copy()
|
|
83
|
+
if columns is None:
|
|
84
|
+
columns = [col for col in df_result.columns if pd.api.types.is_numeric_dtype(df_result[col])]
|
|
85
|
+
|
|
86
|
+
for col in columns:
|
|
87
|
+
if col not in df_result.columns or not pd.api.types.is_numeric_dtype(df_result[col]):
|
|
88
|
+
continue
|
|
89
|
+
series = df_result[col].replace([np.inf, -np.inf], np.nan)
|
|
90
|
+
series_clean = series.dropna()
|
|
91
|
+
if series_clean.empty:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
q1 = series_clean.quantile(0.25)
|
|
95
|
+
q3 = series_clean.quantile(0.75)
|
|
96
|
+
iqr = q3 - q1
|
|
97
|
+
if pd.isna(iqr) or iqr <= 0:
|
|
98
|
+
iqr = 0.0
|
|
99
|
+
lower_limit = q1 - (iqr_factor * iqr)
|
|
100
|
+
upper_limit = q3 + (iqr_factor * iqr)
|
|
101
|
+
|
|
102
|
+
if action == 'flag':
|
|
103
|
+
flag_col = f'{col}_is_outlier'
|
|
104
|
+
if flag_col in df_result.columns:
|
|
105
|
+
raise ValueError(f"Column collision: '{flag_col}' already exists!")
|
|
106
|
+
is_out = (series < lower_limit) | (series > upper_limit)
|
|
107
|
+
df_result[flag_col] = is_out.fillna(False)
|
|
108
|
+
|
|
109
|
+
elif action == 'adjust':
|
|
110
|
+
upper_mask = (series > upper_limit) & (~series.isna())
|
|
111
|
+
lower_mask = (series < lower_limit) & (~series.isna())
|
|
112
|
+
outlier_mask = upper_mask | lower_mask
|
|
113
|
+
natural_mask = (~outlier_mask) & (~series.isna())
|
|
114
|
+
|
|
115
|
+
if outlier_mask.any() or df_result[col].dtype != float:
|
|
116
|
+
df_result[col] = df_result[col].astype(float)
|
|
117
|
+
series = df_result[col]
|
|
118
|
+
|
|
119
|
+
natural_values = series[natural_mask].to_numpy()
|
|
120
|
+
|
|
121
|
+
if osmotic_scale == 'adj_auto':
|
|
122
|
+
if len(natural_values) >= 2:
|
|
123
|
+
n1 = natural_values[:-1]
|
|
124
|
+
n2 = natural_values[1:]
|
|
125
|
+
denoms = n1 + n2
|
|
126
|
+
ratios = np.where(denoms == 0, 0.5, np.abs((n1 - n2) / denoms))
|
|
127
|
+
global_ratio = np.mean(ratios)
|
|
128
|
+
else:
|
|
129
|
+
global_ratio = 0.5
|
|
130
|
+
|
|
131
|
+
if upper_mask.any():
|
|
132
|
+
excess = series[upper_mask] - upper_limit
|
|
133
|
+
df_result.loc[upper_mask, col] = np.abs(global_ratio * excess) + upper_limit
|
|
134
|
+
if lower_mask.any():
|
|
135
|
+
deficit = lower_limit - series[lower_mask]
|
|
136
|
+
df_result.loc[lower_mask, col] = lower_limit - np.abs(global_ratio * deficit)
|
|
137
|
+
|
|
138
|
+
elif osmotic_scale == 'adj_dnmc':
|
|
139
|
+
if len(natural_values) < 2:
|
|
140
|
+
active_ratio = 0.5
|
|
141
|
+
if upper_mask.any():
|
|
142
|
+
excess = series[upper_mask] - upper_limit
|
|
143
|
+
df_result.loc[upper_mask, col] = np.abs(active_ratio * excess) + upper_limit
|
|
144
|
+
if lower_mask.any():
|
|
145
|
+
deficit = lower_limit - series[lower_mask]
|
|
146
|
+
df_result.loc[lower_mask, col] = lower_limit - np.abs(active_ratio * deficit)
|
|
147
|
+
else:
|
|
148
|
+
natural_indices = np.where(natural_mask.to_numpy())[0]
|
|
149
|
+
full_series = series.to_numpy()
|
|
150
|
+
|
|
151
|
+
for idx in np.where(outlier_mask.to_numpy())[0]:
|
|
152
|
+
val = full_series[idx]
|
|
153
|
+
prev_nat_indices = natural_indices[natural_indices < idx]
|
|
154
|
+
if len(prev_nat_indices) >= 2:
|
|
155
|
+
n1 = full_series[prev_nat_indices[-1]]
|
|
156
|
+
n2 = full_series[prev_nat_indices[-2]]
|
|
157
|
+
denom = n1 + n2
|
|
158
|
+
local_ratio = np.abs((n1 - n2) / denom) if denom != 0 else 0.5
|
|
159
|
+
else:
|
|
160
|
+
local_ratio = 0.5
|
|
161
|
+
|
|
162
|
+
if val > upper_limit:
|
|
163
|
+
excess = val - upper_limit
|
|
164
|
+
df_result.iloc[idx, df_result.columns.get_loc(col)] = np.abs(local_ratio * excess) + upper_limit
|
|
165
|
+
elif val < lower_limit:
|
|
166
|
+
deficit = lower_limit - val
|
|
167
|
+
df_result.iloc[idx, df_result.columns.get_loc(col)] = lower_limit - np.abs(local_ratio * deficit)
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
scale_val = float(osmotic_scale)
|
|
171
|
+
if upper_mask.any():
|
|
172
|
+
excess = series[upper_mask] - upper_limit
|
|
173
|
+
df_result.loc[upper_mask, col] = upper_limit + (excess * scale_val)
|
|
174
|
+
if lower_mask.any():
|
|
175
|
+
deficit = lower_limit - series[lower_mask]
|
|
176
|
+
df_result.loc[lower_mask, col] = lower_limit - (deficit * scale_val)
|
|
177
|
+
|
|
178
|
+
df_result[col] = df_result[col].astype(float)
|
|
179
|
+
if round_to_integer:
|
|
180
|
+
df_result[col] = np.round(df_result[col]).astype(float)
|
|
181
|
+
|
|
182
|
+
return df_result
|
|
183
|
+
|
|
184
|
+
def check_skewness_health(df_before, df_after, columns=None):
|
|
185
|
+
if df_before is None or df_after is None:
|
|
186
|
+
return pd.DataFrame()
|
|
187
|
+
if columns is None:
|
|
188
|
+
columns = [col for col in df_before.columns if pd.api.types.is_numeric_dtype(df_before[col])]
|
|
189
|
+
health_report = []
|
|
190
|
+
for col in columns:
|
|
191
|
+
if col in df_before.columns and col in df_after.columns:
|
|
192
|
+
if not pd.api.types.is_numeric_dtype(df_before[col]) or not pd.api.types.is_numeric_dtype(df_after[col]):
|
|
193
|
+
continue
|
|
194
|
+
s_before = df_before[col].replace([np.inf, -np.inf], np.nan).dropna()
|
|
195
|
+
s_after = df_after[col].replace([np.inf, -np.inf], np.nan).dropna()
|
|
196
|
+
if s_before.empty or s_after.empty:
|
|
197
|
+
continue
|
|
198
|
+
health_report.append({
|
|
199
|
+
'Column': col,
|
|
200
|
+
'Skew_Before': round(float(s_before.skew()), 4) if len(s_before) > 2 else np.nan,
|
|
201
|
+
'Skew_After': round(float(s_after.skew()), 4) if len(s_after) > 2 else np.nan,
|
|
202
|
+
'Kurtosis_Before': round(float(s_before.kurtosis()), 4) if len(s_before) > 3 else np.nan,
|
|
203
|
+
'Kurtosis_After': round(float(s_after.kurtosis()), 4) if len(s_after) > 3 else np.nan,
|
|
204
|
+
'Mean_Shift': round(float(s_after.mean() - s_before.mean()), 4),
|
|
205
|
+
'Std_Change': round(float(s_after.std() - s_before.std()), 4)
|
|
206
|
+
})
|
|
207
|
+
return pd.DataFrame(health_report)
|
|
208
|
+
|
|
209
|
+
def plot_osmotic_dashboard(df_before, df_after, column):
|
|
210
|
+
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
|
|
211
|
+
fig.suptitle(f"Osmotic Data Filter Analysis: '{column}'", fontsize=14, fontweight='bold')
|
|
212
|
+
axes[0, 0].set_title("Original Distribution (With Outliers)")
|
|
213
|
+
axes[1, 0].set_title("Original Boxplot")
|
|
214
|
+
axes[0, 1].set_title("Osmotic Adjusted Distribution")
|
|
215
|
+
axes[1, 1].set_title("Osmotic Adjusted Boxplot")
|
|
216
|
+
|
|
217
|
+
if df_before is not None and df_after is not None and column in df_before.columns and column in df_after.columns:
|
|
218
|
+
s_before = df_before[column].replace([np.inf, -np.inf], np.nan).dropna()
|
|
219
|
+
s_after = df_after[column].replace([np.inf, -np.inf], np.nan).dropna()
|
|
220
|
+
if not s_before.empty:
|
|
221
|
+
sns.histplot(s_before, kde=True, ax=axes[0, 0], color='crimson')
|
|
222
|
+
sns.boxplot(x=s_before, ax=axes[1, 0], color='crimson')
|
|
223
|
+
if not s_after.empty:
|
|
224
|
+
sns.histplot(s_after, kde=True, ax=axes[0, 1], color='teal')
|
|
225
|
+
sns.boxplot(x=s_after, ax=axes[1, 1], color='teal')
|
|
226
|
+
|
|
227
|
+
plt.tight_layout()
|
|
228
|
+
return fig
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: osmotic-data-filter
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: A physical chemistry-inspired, safe, and hybrid data bias handling framework
|
|
5
|
+
Author-email: Jana Elkasrawy <kasrawymahmoud@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Requires-Dist: seaborn
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
readme_content = r"""# Osmotic Data Filter (ODF)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
**Osmotic Data Filter (ODF)** is a scientifically grounded, non-destructive data preprocessing Python library designed to handle outliers and anomalies without row deletion or hard clamping. Engineered to meet strict scientific reproducibility standards (e.g., ISEF requirements), ODF compresses extreme statistical deviations toward safe critical boundaries while fully preserving dataset volume, structure, and integrity.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🌟 Key Features (v0.1.8)
|
|
26
|
+
|
|
27
|
+
* **Non-Destructive Outlier Adjustment (`action='adjust'`)**: Compresses outliers toward lower/upper limit boundaries without dropping rows or clamping values to artificial caps.
|
|
28
|
+
* **Smart Scaling Modes (`osmotic_scale`)**: Adaptive scaling designed for both independent and time-series/contextual datasets.
|
|
29
|
+
* **Flexible Post-Adjustment Rounding (`round_to_integer`)**: Optional rounding of adjusted values to nearest integers while strictly preserving `float` data types to prevent DataFrame schema conflicts.
|
|
30
|
+
* **Outlier Flagging (`action='flag'`)**: Adds explicit boolean indicator columns (`<column>_is_outlier`) without altering underlying numerical values.
|
|
31
|
+
* **Comprehensive Visual & Health Metrics**: Built-in functions (`check_skewness_health` and `plot_osmotic_dashboard`) for quantitative skewness reporting and automated dual-panel exploratory dashboards.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🧠Choosing the Right `osmotic_scale` (Guide)
|
|
36
|
+
|
|
37
|
+
ODF provides three strict modes for the `osmotic_scale` parameter, tailored to different data types:
|
|
38
|
+
|
|
39
|
+
1. **`'adj_dnmc'` (Local Dynamic - *Default*):**
|
|
40
|
+
* **When to use:** Ideal for time-series data, sequential measurements, or datasets with local clusters and high local variance.
|
|
41
|
+
* **Why:** It adapts to the local neighborhood of each outlier, preserving structural trends and contextual continuity without destroying local variations.
|
|
42
|
+
|
|
43
|
+
2. **`'adj_auto'` (Global Automatic):**
|
|
44
|
+
* **When to use:** Best for cross-sectional data, independent statistical measurements, or uniform distributions.
|
|
45
|
+
* **Why:** It evaluates global distribution ratios and applies a consistent overall compression logic.
|
|
46
|
+
|
|
47
|
+
3. **Fixed Numeric Scale (`float` between 0 and 1, e.g., `0.3`):**
|
|
48
|
+
* **When to use:** When you need strict, deterministic, or experimentally controlled compression ratios mandated by specific research protocols.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## 🚀 Quick Start Example
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import numpy as np
|
|
57
|
+
import pandas as pd
|
|
58
|
+
import osmotic_data_filter as odf
|
|
59
|
+
|
|
60
|
+
# 1. Create sample dataset with extreme outliers
|
|
61
|
+
data = {
|
|
62
|
+
'feature_col': [10, 12, 11, 14, 15, 12, 900, -400, 13]
|
|
63
|
+
}
|
|
64
|
+
df = pd.DataFrame(data)
|
|
65
|
+
|
|
66
|
+
# 2. Apply Osmotic Data Filtering with Local Dynamic Scale (Default) & Integer Rounding
|
|
67
|
+
df_processed = odf.Osmotic_Data_FLTR(
|
|
68
|
+
df,
|
|
69
|
+
columns=['feature_col'],
|
|
70
|
+
osmotic_scale='adj_dnmc',
|
|
71
|
+
action='adjust',
|
|
72
|
+
round_to_integer=True
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
print(df_processed)
|
|
76
|
+
print("Data Type:", df_processed['feature_col'].dtype)
|
|
77
|
+
|
|
78
|
+
# 3. Check Skewness & Statistical Health
|
|
79
|
+
health_report = odf.check_skewness_health(
|
|
80
|
+
df, df_processed, columns=['feature_col']
|
|
81
|
+
)
|
|
82
|
+
print(health_report)
|
|
83
|
+
|
|
84
|
+
# 4. Render Comparison Dashboard
|
|
85
|
+
fig = odf.plot_osmotic_dashboard(
|
|
86
|
+
df, df_processed, column='feature_col'
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 📖 API Reference
|
|
91
|
+
|
|
92
|
+
### `Osmotic_Data_FLTR(df, columns, osmotic_scale='adj_dnmc', action='adjust', round_to_integer=False)`
|
|
93
|
+
|
|
94
|
+
Main execution function for dataset normalization and adjustment.
|
|
95
|
+
|
|
96
|
+
- **`df`**: Input pandas DataFrame containing numerical features (Required).
|
|
97
|
+
- **`columns`**: List of target column names to inspect and filter (Required).
|
|
98
|
+
- **`osmotic_scale`**: Mode of scaling. Accepts `'adj_auto'`, `'adj_dnmc'`, or float numeric (`0 < scale < 1`). Default is `'adj_dnmc'`.
|
|
99
|
+
- **`action`**: Operation mode. Use `'adjust'` for non-destructive compression or `'flag'` for tagging. Default is `'adjust'`.
|
|
100
|
+
- **`round_to_integer`**: Boolean. If `True`, rounds adjusted values to nearest integers while maintaining `float` dtype. Default is `False`.
|
|
101
|
+
|
|
102
|
+
### `check_skewness_health(df_original, df_filtered, columns)`
|
|
103
|
+
|
|
104
|
+
Computes initial vs. adjusted skewness, IQR variance, and distribution delta metrics.
|
|
105
|
+
|
|
106
|
+
### `plot_osmotic_dashboard(df_original, df_filtered, column)`
|
|
107
|
+
|
|
108
|
+
Generates a publication-ready Matplotlib/Seaborn visualization comparing Original vs. Osmotically Adjusted KDE distributions and Boxplots.
|
|
109
|
+
|
|
110
|
+
## 🔬 Scientific Methodology & Reproducibility
|
|
111
|
+
|
|
112
|
+
1. **Row Count Conservation**: Unlike standard truncation methods, ODF guarantees `N_initial == N_final`, preventing sample bias in downstream analytical or machine learning workflows.
|
|
113
|
+
2. **Schema & Type Consistency**: Enforces continuous mathematical representation (`float64`) regardless of integer rounding flags, ensuring full compatibility with Scikit-Learn, PyTorch, and TensorFlow pipelines.
|
|
114
|
+
3. **Bounded Adjustments**: Strict boundary validation enforces `0 < scale < 1`, preventing divergence or inverse scaling artifacts.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 📜 License
|
|
119
|
+
|
|
120
|
+
Distributed under the **MIT License**. See `LICENSE` file for more information.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# Create README.md
|
|
124
|
+
with open("README.md", "w", encoding="utf-8") as file:
|
|
125
|
+
file.write(readme_content)
|
|
126
|
+
|
|
127
|
+
print("README.md created successfully!")
|
|
128
|
+
|
|
129
|
+
# Download the file
|
|
130
|
+
from google.colab import files
|
|
131
|
+
files.download("README.md")
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
osmotic_data_filter/__init__.py,sha256=lRa8NYH6k3i0BALAcfXQrI58AHytD2vMk33Xg-Lux_s,10646
|
|
2
|
+
osmotic_data_filter-0.1.8.dist-info/METADATA,sha256=Qm0Ro34CrJe4hqdN4_Q9BGDI77C2e6aRgY-P2wsNJJI,5768
|
|
3
|
+
osmotic_data_filter-0.1.8.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
4
|
+
osmotic_data_filter-0.1.8.dist-info/top_level.txt,sha256=fLhYRpz2QLLSHGQoZj6aECa8yio7WbJzn14sa2VSKEY,20
|
|
5
|
+
osmotic_data_filter-0.1.8.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
osmotic_data_filter
|