Kogann commited on
Commit
5bc365f
·
verified ·
1 Parent(s): 0c958d3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -17
app.py CHANGED
@@ -246,8 +246,11 @@ UNSAFE = re.compile(
246
  r"\bimport\s+(os|sys)\b|\bopen\s*\(|\beval\s*\(|\bexec\s*\(|\bgetattr\s*\()"
247
  )
248
 
249
- MAX_ANNOT_COLS = 12 # above this, heatmap cell numbers are unreadable
250
- MAX_TICKS = 20 # above this, thin the tick labels
 
 
 
251
 
252
 
253
  def sanitize(code: str) -> str:
@@ -292,10 +295,53 @@ def _tidy(fig):
292
 
293
  def _install_plot_guards():
294
  """Patch the real modules, so `import matplotlib.pyplot as plt` cannot bypass us."""
295
- real = {"savefig": plt.savefig, "heatmap": sns.heatmap, "histplot": sns.histplot}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  def savefig(fname, *a, **kw):
298
  fig = plt.gcf()
 
 
 
 
 
299
  w, h = fig.get_size_inches()
300
  fig.set_size_inches(max(w, 8), max(h, 5))
301
  _tidy(fig)
@@ -319,19 +365,15 @@ def _install_plot_guards():
319
  rotation=0, fontsize=fs)
320
  return ax
321
 
322
- def _series(a, kw):
323
- d, x = kw.get("data", a[0] if a else None), kw.get("x")
324
- try:
325
- if isinstance(x, str) and hasattr(d, "columns"):
326
- return pd.to_numeric(d[x], errors="coerce").dropna()
327
- if isinstance(d, pd.Series):
328
- return pd.to_numeric(d, errors="coerce").dropna()
329
- except Exception:
330
- pass
331
  return None
332
 
333
  def histplot(*a, **kw):
334
- s = _series(a, kw)
335
  logged = False
336
  # heavy right skew (prices, fares, incomes) -> log x, else one tall bar
337
  if (s is not None and len(s) > 20 and s.min() > 0
@@ -347,7 +389,24 @@ def _install_plot_guards():
347
  ax.set_xlabel(f"{ax.get_xlabel()} (log scale)")
348
  return ax
349
 
350
- plt.savefig, sns.heatmap, sns.histplot = savefig, heatmap, histplot
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  return real
352
 
353
 
@@ -367,13 +426,16 @@ def run_code(code: str, df: pd.DataFrame) -> dict:
367
  except Exception:
368
  err = traceback.format_exc(limit=3)
369
  finally:
 
370
  for i, num in enumerate(plt.get_fignums(), start=1):
371
  fig = plt.figure(num)
372
- if fig.get_axes():
373
  fig.savefig(f"{PLOTS_DIR}/plot_{i}.png")
374
  plt.close("all")
375
- plt.savefig, sns.heatmap, sns.histplot = (
376
- real["savefig"], real["heatmap"], real["histplot"])
 
 
377
 
378
  return {"ok": err is None, "stdout": buf.getvalue(), "error": err,
379
  "plots": sorted(set(glob.glob(f"{PLOTS_DIR}/*.png")) - before)}
 
246
  r"\bimport\s+(os|sys)\b|\bopen\s*\(|\beval\s*\(|\bexec\s*\(|\bgetattr\s*\()"
247
  )
248
 
249
+ MAX_ANNOT_COLS = 12 # above this, heatmap cell numbers are unreadable
250
+ MAX_TICKS = 20 # above this, thin the tick labels
251
+ MAX_CAT_LEVELS = 20 # above this, a categorical plot has nothing to show
252
+ SKIP_ATTR = "_edagent_skip"
253
+ TITLE_ATTR = "_edagent_title"
254
 
255
 
256
  def sanitize(code: str) -> str:
 
295
 
296
  def _install_plot_guards():
297
  """Patch the real modules, so `import matplotlib.pyplot as plt` cannot bypass us."""
298
+ real = {"savefig": plt.savefig, "heatmap": sns.heatmap, "histplot": sns.histplot,
299
+ "boxplot": sns.boxplot, "violinplot": sns.violinplot,
300
+ "countplot": sns.countplot}
301
+
302
+ # ---- helpers ---------------------------------------------------------- #
303
+ def _vars(a, kw):
304
+ """Recover the (name, Series) pairs a seaborn call is plotting."""
305
+ d = kw.get("data", a[0] if a else None)
306
+ out = []
307
+ for key in ("x", "y"):
308
+ v = kw.get(key)
309
+ if isinstance(v, str) and hasattr(d, "columns"):
310
+ out.append((v, d[v]))
311
+ elif isinstance(v, pd.Series):
312
+ out.append((v.name, v))
313
+ if not out and isinstance(d, pd.Series):
314
+ out.append((d.name, d))
315
+ return out
316
+
317
+ def _counts_instead(name, s):
318
+ """A box plot of labels is meaningless; show how often each level occurs."""
319
+ ax = plt.gca()
320
+ s.value_counts().head(MAX_CAT_LEVELS).sort_values().plot(kind="barh", ax=ax)
321
+ ax.set_xlabel("count")
322
+ ax.set_ylabel(str(name or ""))
323
+ setattr(plt.gcf(), TITLE_ATTR, f"Counts of {name}" if name else "Counts")
324
+ return ax
325
 
326
+ def _categorical_guard(a, kw):
327
+ """Return an axes if we handled it, else None to fall through."""
328
+ pairs = _vars(a, kw)
329
+ if not pairs or any(pd.api.types.is_numeric_dtype(s) for _, s in pairs):
330
+ return None # a numeric variable is present: legitimate
331
+ name, s = pairs[0]
332
+ if s.nunique(dropna=True) > MAX_CAT_LEVELS:
333
+ setattr(plt.gcf(), SKIP_ATTR, True) # company_name, free-text descriptions
334
+ return plt.gca()
335
+ return _counts_instead(name, s)
336
+
337
+ # ---- wrappers --------------------------------------------------------- #
338
  def savefig(fname, *a, **kw):
339
  fig = plt.gcf()
340
+ if getattr(fig, SKIP_ATTR, False):
341
+ return None # nothing worth writing to disk
342
+ title = getattr(fig, TITLE_ATTR, None)
343
+ if title and fig.get_axes():
344
+ fig.get_axes()[0].set_title(title) # the model's "Box Plot of X" would lie
345
  w, h = fig.get_size_inches()
346
  fig.set_size_inches(max(w, 8), max(h, 5))
347
  _tidy(fig)
 
365
  rotation=0, fontsize=fs)
366
  return ax
367
 
368
+ def _numeric_series(a, kw):
369
+ for _, s in _vars(a, kw):
370
+ s = pd.to_numeric(s, errors="coerce").dropna()
371
+ if len(s):
372
+ return s
 
 
 
 
373
  return None
374
 
375
  def histplot(*a, **kw):
376
+ s = _numeric_series(a, kw)
377
  logged = False
378
  # heavy right skew (prices, fares, incomes) -> log x, else one tall bar
379
  if (s is not None and len(s) > 20 and s.min() > 0
 
389
  ax.set_xlabel(f"{ax.get_xlabel()} (log scale)")
390
  return ax
391
 
392
+ def boxplot(*a, **kw):
393
+ handled = _categorical_guard(a, kw)
394
+ return handled if handled is not None else real["boxplot"](*a, **kw)
395
+
396
+ def violinplot(*a, **kw):
397
+ handled = _categorical_guard(a, kw)
398
+ return handled if handled is not None else real["violinplot"](*a, **kw)
399
+
400
+ def countplot(*a, **kw):
401
+ pairs = _vars(a, kw)
402
+ if pairs and pairs[0][1].nunique(dropna=True) > MAX_CAT_LEVELS:
403
+ setattr(plt.gcf(), SKIP_ATTR, True)
404
+ return plt.gca()
405
+ return real["countplot"](*a, **kw)
406
+
407
+ plt.savefig = savefig
408
+ sns.heatmap, sns.histplot = heatmap, histplot
409
+ sns.boxplot, sns.violinplot, sns.countplot = boxplot, violinplot, countplot
410
  return real
411
 
412
 
 
426
  except Exception:
427
  err = traceback.format_exc(limit=3)
428
  finally:
429
+ # safety net: plt.show(), or a crash with a figure still open
430
  for i, num in enumerate(plt.get_fignums(), start=1):
431
  fig = plt.figure(num)
432
+ if fig.get_axes() and not getattr(fig, SKIP_ATTR, False):
433
  fig.savefig(f"{PLOTS_DIR}/plot_{i}.png")
434
  plt.close("all")
435
+ plt.savefig = real["savefig"]
436
+ sns.heatmap, sns.histplot = real["heatmap"], real["histplot"]
437
+ sns.boxplot, sns.violinplot = real["boxplot"], real["violinplot"]
438
+ sns.countplot = real["countplot"]
439
 
440
  return {"ok": err is None, "stdout": buf.getvalue(), "error": err,
441
  "plots": sorted(set(glob.glob(f"{PLOTS_DIR}/*.png")) - before)}