whyvalue 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.
- whyvalue/__init__.py +17 -0
- whyvalue/adapters/__init__.py +0 -0
- whyvalue/adapters/pandas.py +1032 -0
- whyvalue/core.py +638 -0
- whyvalue/history.py +15 -0
- whyvalue-0.1.0.dist-info/METADATA +337 -0
- whyvalue-0.1.0.dist-info/RECORD +10 -0
- whyvalue-0.1.0.dist-info/WHEEL +5 -0
- whyvalue-0.1.0.dist-info/licenses/LICENSE +22 -0
- whyvalue-0.1.0.dist-info/top_level.txt +1 -0
whyvalue/core.py
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from .history import history
|
|
4
|
+
from .adapters import pandas as pandas_adapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_watching = False
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def watch():
|
|
11
|
+
"""Start WhyValue tracking."""
|
|
12
|
+
global _watching
|
|
13
|
+
|
|
14
|
+
if _watching:
|
|
15
|
+
print("WhyValue is already watching.")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
history.clear()
|
|
19
|
+
pandas_adapter.enable()
|
|
20
|
+
|
|
21
|
+
_watching = True
|
|
22
|
+
|
|
23
|
+
print("WhyValue is watching your data.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def stop():
|
|
27
|
+
"""Stop WhyValue tracking."""
|
|
28
|
+
global _watching
|
|
29
|
+
|
|
30
|
+
if not _watching:
|
|
31
|
+
print("WhyValue is not currently watching.")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
pandas_adapter.disable()
|
|
35
|
+
|
|
36
|
+
_watching = False
|
|
37
|
+
|
|
38
|
+
print("WhyValue stopped watching.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_watching():
|
|
42
|
+
"""Return whether WhyValue is currently watching."""
|
|
43
|
+
return _watching
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_symbol(operation):
|
|
47
|
+
symbols = {
|
|
48
|
+
"add": "+",
|
|
49
|
+
"multiply": "×",
|
|
50
|
+
"subtract": "-",
|
|
51
|
+
"divide": "/",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return symbols.get(operation, "?")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_dataframe_id(obj):
|
|
58
|
+
"""Return the WhyValue lineage ID stored on a DataFrame or Series."""
|
|
59
|
+
if not hasattr(obj, "attrs"):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
return obj.attrs.get("_whyvalue_id") or obj.attrs.get("_whyvalue_dataframe_id")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _event_matches_dataframe(event, dataframe_id):
|
|
66
|
+
"""Return whether an event belongs to a DataFrame lineage."""
|
|
67
|
+
if dataframe_id is None:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
return event.get("dataframe_id") == dataframe_id
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _find_event(obj, column):
|
|
74
|
+
"""Find the most recent transformation for a column."""
|
|
75
|
+
dataframe_id = _get_dataframe_id(obj)
|
|
76
|
+
events = history.get_all()
|
|
77
|
+
|
|
78
|
+
for event in reversed(events):
|
|
79
|
+
if (
|
|
80
|
+
_event_matches_dataframe(event, dataframe_id)
|
|
81
|
+
and (
|
|
82
|
+
event.get("column") == column
|
|
83
|
+
or (event["type"] == "rename" and column in event["columns"].values())
|
|
84
|
+
or event["type"] == "merge"
|
|
85
|
+
or (event["type"] == "groupby" and event.get("source") == column)
|
|
86
|
+
)
|
|
87
|
+
and event["type"] != "filter"
|
|
88
|
+
):
|
|
89
|
+
return event
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _find_events_by_dataframe_id(dataframe_id, column):
|
|
95
|
+
"""Find all matching events for a dataframe ID + column in chronological order."""
|
|
96
|
+
if dataframe_id is None:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
events = history.get_all()
|
|
100
|
+
matching = []
|
|
101
|
+
|
|
102
|
+
for event in events:
|
|
103
|
+
if (
|
|
104
|
+
_event_matches_dataframe(event, dataframe_id)
|
|
105
|
+
and (
|
|
106
|
+
event.get("column") == column
|
|
107
|
+
or (event["type"] == "rename" and column in event["columns"].values())
|
|
108
|
+
or event["type"] == "merge"
|
|
109
|
+
or (event["type"] == "groupby" and event.get("source") == column)
|
|
110
|
+
)
|
|
111
|
+
and event["type"] != "filter"
|
|
112
|
+
):
|
|
113
|
+
matching.append(event)
|
|
114
|
+
|
|
115
|
+
return matching
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _find_events(obj, column):
|
|
119
|
+
"""Find all transformations for a column in chronological order."""
|
|
120
|
+
return _find_events_by_dataframe_id(_get_dataframe_id(obj), column)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _find_event_by_type(obj, column, event_type):
|
|
124
|
+
"""Find the most recent event of a specific type."""
|
|
125
|
+
dataframe_id = _get_dataframe_id(obj)
|
|
126
|
+
events = history.get_all()
|
|
127
|
+
|
|
128
|
+
for event in reversed(events):
|
|
129
|
+
if (
|
|
130
|
+
_event_matches_dataframe(event, dataframe_id)
|
|
131
|
+
and event.get("column") == column
|
|
132
|
+
and event["type"] == event_type
|
|
133
|
+
):
|
|
134
|
+
return event
|
|
135
|
+
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _find_filter_for_row(row, obj=None):
|
|
140
|
+
"""Find the most recent row-removal event."""
|
|
141
|
+
events = history.get_all()
|
|
142
|
+
|
|
143
|
+
dataframe_id = None
|
|
144
|
+
|
|
145
|
+
if obj is not None:
|
|
146
|
+
dataframe_id = _get_dataframe_id(obj)
|
|
147
|
+
|
|
148
|
+
for event in reversed(events):
|
|
149
|
+
if event["type"] not in ("filter", "dropna", "combined_filter"):
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if not _event_matches_dataframe(
|
|
153
|
+
event,
|
|
154
|
+
dataframe_id,
|
|
155
|
+
):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
if row in event["removed_rows"]:
|
|
159
|
+
return event
|
|
160
|
+
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def trace(obj):
|
|
165
|
+
"""Show transformations belonging to a DataFrame."""
|
|
166
|
+
dataframe_id = _get_dataframe_id(obj)
|
|
167
|
+
|
|
168
|
+
events = [
|
|
169
|
+
event
|
|
170
|
+
for event in history.get_all()
|
|
171
|
+
if _event_matches_dataframe(
|
|
172
|
+
event,
|
|
173
|
+
dataframe_id,
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
if not events:
|
|
178
|
+
print("No transformations recorded.")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
print("WhyValue Trace")
|
|
182
|
+
print()
|
|
183
|
+
|
|
184
|
+
for event in events:
|
|
185
|
+
if event["type"] == "column_created":
|
|
186
|
+
symbol = _get_symbol(event["operation"])
|
|
187
|
+
|
|
188
|
+
print(f"{event['column']} = {event['left']} {symbol} {event['right']}")
|
|
189
|
+
|
|
190
|
+
elif event["type"] == "column_filled":
|
|
191
|
+
print(f"{event['column']}: missing values → {event['value']}")
|
|
192
|
+
|
|
193
|
+
elif event["type"] == "filter":
|
|
194
|
+
print(f"filter: {event['column']} {event['operator']} {event['value']}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _explain_column(obj, row, column):
|
|
198
|
+
"""Explain one column and recursively explain dependencies."""
|
|
199
|
+
event = _find_event(obj, column)
|
|
200
|
+
|
|
201
|
+
if event is None:
|
|
202
|
+
value = obj.loc[row, column] if not isinstance(obj, pd.Series) else obj.loc[row]
|
|
203
|
+
print(f"{column} = {value}")
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if event["type"] == "column_created":
|
|
207
|
+
if event.get("operation") == "copy":
|
|
208
|
+
result_value = (
|
|
209
|
+
obj.loc[row, column] if not isinstance(obj, pd.Series) else obj.loc[row]
|
|
210
|
+
)
|
|
211
|
+
source = event["source"]
|
|
212
|
+
print("Copied from column:")
|
|
213
|
+
print(source)
|
|
214
|
+
print()
|
|
215
|
+
print("Source: another DataFrame")
|
|
216
|
+
print()
|
|
217
|
+
print(f"{column} = {result_value}")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
symbol = _get_symbol(event["operation"])
|
|
221
|
+
|
|
222
|
+
left_type = event.get(
|
|
223
|
+
"left_type",
|
|
224
|
+
"column",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
right_type = event.get(
|
|
228
|
+
"right_type",
|
|
229
|
+
"column",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
current_df_id = _get_dataframe_id(obj)
|
|
233
|
+
|
|
234
|
+
left_df_id = event.get("left_dataframe_id")
|
|
235
|
+
is_left_local = False
|
|
236
|
+
if left_type == "scalar":
|
|
237
|
+
is_left_local = True
|
|
238
|
+
left_value = event["left"]
|
|
239
|
+
else:
|
|
240
|
+
left_column = event["left"]
|
|
241
|
+
if (
|
|
242
|
+
(left_df_id is None or left_df_id == current_df_id)
|
|
243
|
+
and hasattr(obj, "columns")
|
|
244
|
+
and left_column in obj.columns
|
|
245
|
+
):
|
|
246
|
+
is_left_local = True
|
|
247
|
+
_explain_column(
|
|
248
|
+
obj,
|
|
249
|
+
row,
|
|
250
|
+
left_column,
|
|
251
|
+
)
|
|
252
|
+
left_value = (
|
|
253
|
+
obj.loc[row, left_column]
|
|
254
|
+
if not isinstance(obj, pd.Series)
|
|
255
|
+
else obj.loc[row]
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
is_left_local = False
|
|
259
|
+
left_value = None
|
|
260
|
+
|
|
261
|
+
right_df_id = event.get("right_dataframe_id")
|
|
262
|
+
is_right_local = False
|
|
263
|
+
if right_type == "scalar":
|
|
264
|
+
is_right_local = True
|
|
265
|
+
right_value = event["right"]
|
|
266
|
+
else:
|
|
267
|
+
right_column = event["right"]
|
|
268
|
+
if (
|
|
269
|
+
(right_df_id is None or right_df_id == current_df_id)
|
|
270
|
+
and hasattr(obj, "columns")
|
|
271
|
+
and right_column in obj.columns
|
|
272
|
+
):
|
|
273
|
+
is_right_local = True
|
|
274
|
+
_explain_column(
|
|
275
|
+
obj,
|
|
276
|
+
row,
|
|
277
|
+
right_column,
|
|
278
|
+
)
|
|
279
|
+
right_value = (
|
|
280
|
+
obj.loc[row, right_column]
|
|
281
|
+
if not isinstance(obj, pd.Series)
|
|
282
|
+
else obj.loc[row]
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
is_right_local = False
|
|
286
|
+
right_value = None
|
|
287
|
+
|
|
288
|
+
if left_type == "column" and not is_left_local:
|
|
289
|
+
print(f"{event['left']} came from another DataFrame.")
|
|
290
|
+
print()
|
|
291
|
+
|
|
292
|
+
if right_type == "column" and not is_right_local:
|
|
293
|
+
print(f"{event['right']} came from another DataFrame.")
|
|
294
|
+
print()
|
|
295
|
+
|
|
296
|
+
result_value = (
|
|
297
|
+
obj.loc[row, column] if not isinstance(obj, pd.Series) else obj.loc[row]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if is_left_local and is_right_local:
|
|
301
|
+
print()
|
|
302
|
+
print(f"{left_value} {symbol} {right_value}")
|
|
303
|
+
print(f"→ {column} = {result_value}")
|
|
304
|
+
else:
|
|
305
|
+
print("Operation:")
|
|
306
|
+
print(f"{event['left']} {symbol} {event['right']}")
|
|
307
|
+
print()
|
|
308
|
+
print(f"→ {column} = {result_value}")
|
|
309
|
+
|
|
310
|
+
elif event["type"] == "column_filled":
|
|
311
|
+
result_value = obj.loc[row, column]
|
|
312
|
+
fill_value = event["value"]
|
|
313
|
+
|
|
314
|
+
if row in event["missing_rows"]:
|
|
315
|
+
print("Original value:")
|
|
316
|
+
print("NaN")
|
|
317
|
+
print()
|
|
318
|
+
|
|
319
|
+
print("Transformation:")
|
|
320
|
+
print(f"fillna({fill_value})")
|
|
321
|
+
print()
|
|
322
|
+
|
|
323
|
+
print(f"NaN → {result_value}")
|
|
324
|
+
|
|
325
|
+
else:
|
|
326
|
+
print(f"{column} = {result_value}")
|
|
327
|
+
print()
|
|
328
|
+
|
|
329
|
+
print(f"fillna({fill_value}) did not change this row.")
|
|
330
|
+
|
|
331
|
+
elif event["type"] == "astype":
|
|
332
|
+
result_value = obj.loc[row, column]
|
|
333
|
+
|
|
334
|
+
print("Transformation:")
|
|
335
|
+
print(f"astype({event['dtype']})")
|
|
336
|
+
print()
|
|
337
|
+
print(f"{event['source']} → {column}")
|
|
338
|
+
print(f"{column} = {result_value}")
|
|
339
|
+
|
|
340
|
+
elif event["type"] == "round":
|
|
341
|
+
result_value = obj.loc[row, column]
|
|
342
|
+
|
|
343
|
+
print("Transformation:")
|
|
344
|
+
print(f"round({event['decimals']})")
|
|
345
|
+
print()
|
|
346
|
+
print(f"{event['source']} → {column}")
|
|
347
|
+
print(f"{column} = {result_value}")
|
|
348
|
+
|
|
349
|
+
elif event["type"] == "rename":
|
|
350
|
+
source = next(
|
|
351
|
+
source
|
|
352
|
+
for source, destination in event["columns"].items()
|
|
353
|
+
if destination == column
|
|
354
|
+
)
|
|
355
|
+
result_value = obj.loc[row, column]
|
|
356
|
+
|
|
357
|
+
print("Transformation:")
|
|
358
|
+
print("rename()")
|
|
359
|
+
print()
|
|
360
|
+
print(f"{source} → {column}")
|
|
361
|
+
print(f"{column} = {result_value}")
|
|
362
|
+
|
|
363
|
+
elif event["type"] == "map":
|
|
364
|
+
result_value = obj.loc[row, column]
|
|
365
|
+
|
|
366
|
+
print("Transformation:")
|
|
367
|
+
print("map()")
|
|
368
|
+
print()
|
|
369
|
+
print("Mapping:")
|
|
370
|
+
for key, value in event["mapping"].items():
|
|
371
|
+
print(f"{key} → {value}")
|
|
372
|
+
|
|
373
|
+
print()
|
|
374
|
+
print(f"{event['source']} → {column}")
|
|
375
|
+
print(f"{column} = {result_value}")
|
|
376
|
+
|
|
377
|
+
elif event["type"] == "apply":
|
|
378
|
+
result_value = obj.loc[row, column]
|
|
379
|
+
|
|
380
|
+
print("Transformation:")
|
|
381
|
+
print(f"apply({event['function']})")
|
|
382
|
+
print()
|
|
383
|
+
print(f"{event['source']} → {column}")
|
|
384
|
+
print(f"{column} = {result_value}")
|
|
385
|
+
|
|
386
|
+
elif event["type"] == "merge":
|
|
387
|
+
result_value = obj.loc[row, column]
|
|
388
|
+
|
|
389
|
+
how = event.get("how", "inner")
|
|
390
|
+
on = event.get("on")
|
|
391
|
+
|
|
392
|
+
if on:
|
|
393
|
+
if isinstance(on, list):
|
|
394
|
+
on_str = ", ".join(on)
|
|
395
|
+
else:
|
|
396
|
+
on_str = str(on)
|
|
397
|
+
merge_desc = f"{how} join on {on_str}"
|
|
398
|
+
else:
|
|
399
|
+
merge_desc = f"{how} join"
|
|
400
|
+
|
|
401
|
+
print("Merge:")
|
|
402
|
+
print(merge_desc)
|
|
403
|
+
print()
|
|
404
|
+
print("This DataFrame was created by combining two data sources.")
|
|
405
|
+
print()
|
|
406
|
+
print(f"{column} = {result_value}")
|
|
407
|
+
|
|
408
|
+
elif event["type"] == "groupby":
|
|
409
|
+
if isinstance(obj, pd.Series):
|
|
410
|
+
result_value = obj.loc[row]
|
|
411
|
+
else:
|
|
412
|
+
result_value = obj.loc[row, column]
|
|
413
|
+
|
|
414
|
+
group_by = event["group_by"]
|
|
415
|
+
aggregation = event["aggregation"]
|
|
416
|
+
|
|
417
|
+
source_df_id = event.get("source_dataframe_id")
|
|
418
|
+
source_col = event.get("source")
|
|
419
|
+
|
|
420
|
+
source_events = []
|
|
421
|
+
if source_df_id and source_col:
|
|
422
|
+
source_events = _find_events_by_dataframe_id(source_df_id, source_col)
|
|
423
|
+
|
|
424
|
+
if source_events:
|
|
425
|
+
print("Source transformation history:")
|
|
426
|
+
print()
|
|
427
|
+
for idx, src_event in enumerate(source_events, start=1):
|
|
428
|
+
print(f"{idx}. {_format_event_summary(src_event)}")
|
|
429
|
+
|
|
430
|
+
print()
|
|
431
|
+
print("Aggregation:")
|
|
432
|
+
print(f"{aggregation}()")
|
|
433
|
+
print()
|
|
434
|
+
print("Grouped by:")
|
|
435
|
+
print(f"{group_by} = {row}")
|
|
436
|
+
print()
|
|
437
|
+
print("Final:")
|
|
438
|
+
print(f"{column} = {result_value}")
|
|
439
|
+
else:
|
|
440
|
+
print("Aggregation:")
|
|
441
|
+
print(f"{aggregation}()")
|
|
442
|
+
print()
|
|
443
|
+
print("Grouped by:")
|
|
444
|
+
print(f"{group_by} = {row}")
|
|
445
|
+
print()
|
|
446
|
+
print(f"{column} = {result_value}")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _format_event_summary(event):
|
|
450
|
+
"""Format an event into a single line summary step."""
|
|
451
|
+
event_type = event.get("type")
|
|
452
|
+
|
|
453
|
+
if event_type == "column_created":
|
|
454
|
+
op = event.get("operation")
|
|
455
|
+
if op == "copy":
|
|
456
|
+
return f"copy from {event.get('source')}"
|
|
457
|
+
|
|
458
|
+
symbol = _get_symbol(op)
|
|
459
|
+
left = event.get("left")
|
|
460
|
+
right = event.get("right")
|
|
461
|
+
col = event.get("column")
|
|
462
|
+
return f"{left} {symbol} {right} → {col}"
|
|
463
|
+
|
|
464
|
+
elif event_type == "column_filled":
|
|
465
|
+
return f"fillna({event.get('value')})"
|
|
466
|
+
|
|
467
|
+
elif event_type == "astype":
|
|
468
|
+
return f"astype({event.get('dtype')})"
|
|
469
|
+
|
|
470
|
+
elif event_type == "round":
|
|
471
|
+
return f"round({event.get('decimals')})"
|
|
472
|
+
|
|
473
|
+
elif event_type == "map":
|
|
474
|
+
return "map()"
|
|
475
|
+
|
|
476
|
+
elif event_type == "apply":
|
|
477
|
+
return f"apply({event.get('function')})"
|
|
478
|
+
|
|
479
|
+
elif event_type == "rename":
|
|
480
|
+
return "rename()"
|
|
481
|
+
|
|
482
|
+
elif event_type == "merge":
|
|
483
|
+
return "merge()"
|
|
484
|
+
|
|
485
|
+
elif event_type == "groupby":
|
|
486
|
+
return f"{event.get('aggregation')}()"
|
|
487
|
+
|
|
488
|
+
return f"{event_type}()"
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _explain_event_chain(obj, row, column, events):
|
|
492
|
+
"""Explain a sequence of transformations on the same column."""
|
|
493
|
+
if isinstance(obj, pd.Series):
|
|
494
|
+
result_value = obj.loc[row]
|
|
495
|
+
else:
|
|
496
|
+
result_value = obj.loc[row, column]
|
|
497
|
+
|
|
498
|
+
print("Transformation history:")
|
|
499
|
+
print()
|
|
500
|
+
for idx, event in enumerate(events, start=1):
|
|
501
|
+
print(f"{idx}. {_format_event_summary(event)}")
|
|
502
|
+
|
|
503
|
+
print()
|
|
504
|
+
print("Final:")
|
|
505
|
+
print(f"{column} = {result_value}")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def explain(obj, row, column=None):
|
|
509
|
+
"""Explain why a specific cell has its current value."""
|
|
510
|
+
if isinstance(obj, pd.Series) and column is None:
|
|
511
|
+
column = obj.name
|
|
512
|
+
|
|
513
|
+
events = _find_events(
|
|
514
|
+
obj,
|
|
515
|
+
column,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if not events:
|
|
519
|
+
print(f"No explanation found for '{column}'.")
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
if isinstance(obj, pd.Series):
|
|
523
|
+
result_value = obj.loc[row]
|
|
524
|
+
else:
|
|
525
|
+
result_value = obj.loc[row, column]
|
|
526
|
+
|
|
527
|
+
print(f"Why is {column} = {result_value}?")
|
|
528
|
+
print()
|
|
529
|
+
|
|
530
|
+
if len(events) == 1:
|
|
531
|
+
_explain_column(
|
|
532
|
+
obj,
|
|
533
|
+
row,
|
|
534
|
+
column,
|
|
535
|
+
)
|
|
536
|
+
else:
|
|
537
|
+
_explain_event_chain(
|
|
538
|
+
obj,
|
|
539
|
+
row,
|
|
540
|
+
column,
|
|
541
|
+
events,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def explain_removed(obj=None, row=None):
|
|
546
|
+
"""Explain why a row was removed by a filter or dropna."""
|
|
547
|
+
|
|
548
|
+
if row is None:
|
|
549
|
+
print("Please provide a row.")
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
filter_event = _find_filter_for_row(
|
|
553
|
+
row,
|
|
554
|
+
obj,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
if filter_event is None:
|
|
558
|
+
print(f"No removal explanation found for row {row}.")
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
if filter_event["type"] == "dropna":
|
|
562
|
+
print(f"Why was row {row} removed?")
|
|
563
|
+
print()
|
|
564
|
+
print(f"dropna(subset={filter_event['subset']})")
|
|
565
|
+
print()
|
|
566
|
+
print("Row removed because required data was missing.")
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
if filter_event["type"] == "combined_filter":
|
|
570
|
+
print(f"Why was row {row} removed?")
|
|
571
|
+
print()
|
|
572
|
+
print(f"Combined filter ({filter_event['logic'].upper()}):")
|
|
573
|
+
print()
|
|
574
|
+
|
|
575
|
+
for condition in filter_event["conditions"]:
|
|
576
|
+
print(f"{condition['column']} {condition['operator']} {condition['value']}")
|
|
577
|
+
|
|
578
|
+
print()
|
|
579
|
+
if filter_event["logic"] == "and":
|
|
580
|
+
print("Row removed because the row did not satisfy all conditions.")
|
|
581
|
+
|
|
582
|
+
elif filter_event["logic"] == "or":
|
|
583
|
+
print("Row removed because the row did not satisfy any condition.")
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
column = filter_event["column"]
|
|
587
|
+
operator = filter_event["operator"]
|
|
588
|
+
threshold = filter_event["value"]
|
|
589
|
+
row_value = filter_event["removed_values"][row]
|
|
590
|
+
|
|
591
|
+
print(f"Why was row {row} removed?")
|
|
592
|
+
print()
|
|
593
|
+
|
|
594
|
+
fill_event = None
|
|
595
|
+
|
|
596
|
+
if obj is not None:
|
|
597
|
+
fill_event = _find_event_by_type(
|
|
598
|
+
obj,
|
|
599
|
+
column,
|
|
600
|
+
"column_filled",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
if fill_event is None:
|
|
604
|
+
dataframe_id = filter_event.get("source_dataframe_id") or filter_event.get(
|
|
605
|
+
"dataframe_id"
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
for event in reversed(history.get_all()):
|
|
609
|
+
if (
|
|
610
|
+
event.get("dataframe_id") == dataframe_id
|
|
611
|
+
and event.get("column") == column
|
|
612
|
+
and event["type"] == "column_filled"
|
|
613
|
+
):
|
|
614
|
+
fill_event = event
|
|
615
|
+
break
|
|
616
|
+
|
|
617
|
+
if fill_event is not None and row in fill_event["missing_rows"]:
|
|
618
|
+
fill_value = fill_event["value"]
|
|
619
|
+
|
|
620
|
+
print(f"Original {column}:")
|
|
621
|
+
print("NaN")
|
|
622
|
+
print()
|
|
623
|
+
|
|
624
|
+
print("Transformation:")
|
|
625
|
+
print(f"fillna({fill_value})")
|
|
626
|
+
print()
|
|
627
|
+
|
|
628
|
+
print(f"NaN → {row_value}")
|
|
629
|
+
print()
|
|
630
|
+
|
|
631
|
+
print("Filter:")
|
|
632
|
+
print(f"{column} {operator} {threshold}")
|
|
633
|
+
print()
|
|
634
|
+
|
|
635
|
+
print(f"{row_value} {operator} {threshold} → False")
|
|
636
|
+
print()
|
|
637
|
+
|
|
638
|
+
print("Row removed")
|
whyvalue/history.py
ADDED