Reproducing Kim et al. (2022) from dandiset 000489¶
Paper. Kim V, Semenov I, Kiester AS, Keppler MA, Ibey BL, Bixler JN, Pakhomov AG (2022). Action spectra and mechanisms of (in)efficiency of bipolar electric pulses at electroporation. Bioelectrochemistry 149:108319. doi:10.1016/j.bioelechem.2022.108319. The claims file was written from the accepted author manuscript in PubMed Central (PMC9729435), which holds the main text, the Methods and the figure legends; the paper has no supplement and links no code.
Dandiset. DANDI:000489, version 0.230518.1811 (doi:10.48324/dandi.000489/0.230518.1811). The paper cites no version. This is the only published version, and the draft holds the same 18 files (same paths, sizes and blobs; the check is printed below), so the most recent published version is pinned.
What the dandiset holds. One stitched two-channel image (MemBrite and YO-PRO-1) of the whole
glass bottom of each of 17 dishes exposed to 5 pulses at 1 Hz, unipolar 600 ns or bipolar
600+600 ns with a second phase of 25, 50, 75 or 100%: the experiment of Fig. 2. An 18th file is a
sample image named for Fig. 1F. The experiments of Figs. 3 to 6 (pulse number, duration, the aa'
line, repetition rate), the strobe imaging of Fig. 7 and the thermometry of Fig. 1E are not in it.
Those of Figs. 3 to 7 are in four other dandisets that name the paper (000548, 000549, 000550 and
000448; listed in the data check). They are separate dandisets and separate targets of the
registry, and the claims file does not name them as a source for this one, so they are not used.
The files hold no ROI, no field values, no pixel size and no footprint values: everything is
measured here from the images, and the field of each ROI is recomputed from the electrode geometry
of Methods 2.3 (see bipolar.py).
Method. Electrode footprints are found as the dark discs of the YP image. ROI of 200 um are
tiled from the midpoint of the electrode centres along bb', the perpendicular to the electrode
axis, out to 3 mm on each side. In each ROI, YP = YP_ROI x MB_0 / MB_ROI - YP_0 (Methods 2.5),
with YP_0 and MB_0 from the footprints. The file sub-Fig2-0-03 is left out of the main analysis
because its YP channel is a copy of another dish's (data check). Dandiset 000548 holds the YP
image of that dish; it is checked, not used.
| Claim | Figure | Status |
|---|---|---|
| fig2a-uptake-rises-with-field | 2A | tested |
| fig2a-vertical-scales | 2A | tested |
| fig2b-exponential-same-slope | 2B | tested |
| fig2c-max-inhibition-50-75 | 2C | tested |
| fig2c-inhibition-flat-above-8-7 | 2C | tested |
| fig2c-weaker-at-6-7 | 2C | analysed; blocked: other (unipolar mean at background at 6.7 kV/cm) |
| fig3-uptake-linear-in-pulse-number, fig3d-cancellation-independent-of-number | 3 | blocked: other (data in another dandiset of the paper) |
| fig4ac-ns-bipolar-20-25pct, fig4d-1200ns-tenfold, fig4eg-us-bipolar-30-50pct, fig4h-us-stronger-at-low-field | 4 | blocked: other (data in another dandiset of the paper) |
| fig5-ns-symmetric-along-aa, fig5-us-anode-stronger | 5 | blocked: other (data in another dandiset of the paper) |
| fig6ad-unipolar-833khz-enhanced, fig6be-bipolar-khz-less-than-1hz, fig6cf-833khz-thirtyfold, fig6cf-cancellation-stronger-at-high-field | 6 | blocked: other (data in another dandiset of the paper) |
| fig7-charging-time-constant, fig7-bipolar-overshoot, fig7-summation-without-buildup | 7 | blocked: other (data in another dandiset of the paper) |
| fig1e-heating | 1E | blocked: data missing |
import time
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import requests
from scipy import optimize, stats
import bipolar as bp
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
warnings.filterwarnings("ignore", category=RuntimeWarning)
started = time.time()
dishes = bp.load_all()
print(f"loaded {len(dishes)} files from dandiset {bp.DANDISET_ID} version {bp.DANDISET_VERSION} "
f"in {time.time() - started:.0f} s")
loaded 18 files from dandiset 000489 version 0.230518.1811 in 558 s
Data check¶
Each check of the metadata against the values is stated under its output.
meta = bp.dandiset_metadata()
draft = bp.dandiset_metadata("draft")
print("Title:", meta["name"])
print("Description:", meta["description"])
print("Related resource:", [(r["relation"], r["url"]) for r in meta.get("relatedResource", [])])
draft_assets = sorted((a["path"], a["size"], a.get("blob")) for a in
requests.get(f"{bp.API}/dandisets/{bp.DANDISET_ID}/versions/draft/assets/?page_size=100").json()["results"])
print("Draft holds the same files (path, size, blob) as", bp.DANDISET_VERSION, ":",
draft_assets == sorted((a["path"], a["size"], a["blob"]) for a in bp.assets()))
print("Metadata fields that differ between draft and published:",
sorted(k for k in set(meta) | set(draft) if meta.get(k) != draft.get(k)))
info = pd.DataFrame([{
"subject": d["subject"], "amplitude_%": d["amplitude"], "acquisition": ",".join(d["meta"]["acquisition"]),
"session_id": d["meta"]["session_id"], "date": d["meta"]["session_start"][:16],
"shape": d["meta"]["shape"], "dtype": d["meta"]["dtype"], "unit": d["meta"]["unit"],
"conversion": d["meta"]["conversion"], "rate": d["meta"]["rate"],
"other_groups": d["meta"]["processing"] + d["meta"]["analysis"] + d["meta"]["stimulus"]} for d in dishes])
print("\nFiles:", len(info))
print(info.to_string(index=False))
print("\nDishes per second phase amplitude (Fig. 2 files):")
print(info["amplitude_%"].value_counts().sort_index().to_string())
print("\nSeries description (first Fig. 2 file):", dishes[1]["meta"]["description"])
print("Session description (first Fig. 2 file):", dishes[1]["meta"]["session_description"])
Title: The impact of the second phase amplitude (% to the first phase) on the electroporation efficiency (measured as YP emission)
Description: Cells were electroporated by a train of 5 unipolar 600 ns pulses (0 percent) or bipolar 600 + 600 ns pulses (25, 50, 75, 100 percent) at 1 Hz. 25, 50, 75, and 100% represent the second pulse's amplitude in relation to the first pulse's amplitude. 0% represents a unipolar pulse, where no second pulse was applied. The electrodes were two parallel stainless-steel cylinders, 1.5 mm in diameter and 3 mm apart. Supported by NIH 1R21EY034258
Related resource: [('dcite:IsPublishedIn', 'https://doi.org/10.1016/j.bioelechem.2022.108319')]
Draft holds the same files (path, size, blob) as 0.230518.1811 : True
Metadata fields that differ between draft and published: ['citation', 'doi', 'id', 'manifestLocation', 'url', 'version']
Files: 18
subject amplitude_% acquisition session_id date shape dtype unit conversion rate other_groups
sub-Fig1F NaN Channel1 and Channel2 BPAE_staining 2021-09-01T19:13 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-0-03 0.0 Fig2_0_03 BPAE_bipolar_0 percent 2021-07-29T13:00 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-0-06 0.0 Fig2_0_06 BPAE_bipolar_0 percent 2021-07-29T13:46 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-0-07 0.0 Fig2_0_07 BPAE_bipolar_0 percent 2021-07-29T14:00 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-100-03 100.0 Fig2_100_03 BPAE_bipolar_100 percent 2021-08-24T15:18 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-100-07 100.0 Fig2_100_07 BPAE_bipolar_100 percent 2021-08-24T16:16 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-100-08 100.0 Fig2_100_08 BPAE_bipolar_100 percent 2021-08-24T16:31 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-25-04 25.0 Fig2_25 BPAE_bipolar_25 percent 2021-07-26T17:25 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-25-10 25.0 Fig2_25 BPAE_bipolar_25 percent 2021-07-29T16:24 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-25-14 25.0 Fig2_25 BPAE_bipolar_25 percent 2021-07-29T17:22 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-50-06 50.0 Fig2_50_06 BPAE_bipolar_50 percent 2021-07-26T18:52 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-50-10 50.0 Fig2_50_10 BPAE_bipolar_50 percent 2021-07-26T19:52 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-50-12 50.0 Fig2_50_12 BPAE_bipolar_50 percent 2021-07-26T20:21 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-50-13 50.0 Fig2_50_13 BPAE_bipolar_50 percent 2021-07-29T17:07 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-50-18 50.0 Fig2_50_18 BPAE_bipolar_50 percent 2021-07-29T18:06 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-75-02 75.0 Fig2_75_02 BPAE_bipolar_75 percent 2021-08-24T15:01 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-75-05 75.0 Fig2_75_05 BPAE_bipolar_75 percent 2021-08-24T15:47 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
sub-Fig2-75-10 75.0 Fig2_75_10 BPAE_bipolar_75 percent 2021-08-24T17:00 (22323, 22323, 2) uint16 n.a. 1.0 2.0 []
Dishes per second phase amplitude (Fig. 2 files):
amplitude_%
0.0 3
25.0 3
50.0 5
75.0 3
100.0 3
Series description (first Fig. 2 file): Channel1 ex/em 350/460nm and ex/em 480/535nm. All cells labeled with MemBrite dye (Channel 1), permeabilized cells visualized by membrane-impermeable fluorescence dye YOPRO-1, Dark circles with no electroporated cells are the footprints of electrodes. An image was obtained by scanning a whole well from a six-well plate, and a total of 144 images (12x12 grid) were captured. These images were then automatically stitched together to create a composite image.
Session description (first Fig. 2 file): Cells were electroporated by a train of 5 unipolar 600 ns pulses (no second pulse(0%)) at 1 Hz.The electrodes were two parallel stainless-steel cylinders, 1.5 mm in diameter and 3 mm apart
The dandiset holds 18 files: one stitched image of the whole glass bottom per dish for the 17
dishes of Fig. 2 (0% second phase: 3, 25%: 3, 50%: 5, 75%: 3, 100%: 3, inside the paper's "3-8"
exposures per protocol) and one sample image named for Fig. 1F. Each file holds one ImageSeries
whose data is a single two-channel image stored as (22323, 22323, 2) with a "rate" of 2 Hz: as an
ImageSeries the first axis is time, so the file reads as 22323 frames of 22323 x 2 pixels. The
image has no pixel size, unit ("n.a."), electrode position, ROI, field map, pulse voltage or
footprint values; nothing is in processing, analysis or stimulus. The series description
says the image was stitched from 144 images (12 x 12) of "a whole well from a six-well plate";
the paper says 216 images of a 35 mm dish (Methods 2.1 and 2.5). The acquisition name is not unique
(the three 25% files all name it Fig2_25). There are no time series and no trial table, so the
check of a shared clock does not apply: each file is one image taken after the exposure.
fig, axes = plt.subplots(3, 6, figsize=(18, 9))
for ax, d in zip(axes.flat, dishes):
im = d["overview"][bp.YP]
lo, hi = np.percentile(im[im > 0], [5, 99.5])
ax.imshow(im, vmin=lo, vmax=hi, cmap="gray")
if "footprints" in d:
for c, r in zip(d["footprints"]["centres"], d["footprints"]["radii"]):
ax.add_patch(plt.Circle((c[1] / bp.BLOCK[1], c[0] / bp.BLOCK[0]), r / 24, fill=False, color="r"))
for roi in d["measure"]["rois"]:
ax.plot(roi["col"] / bp.BLOCK[1], roi["row"] / bp.BLOCK[0], ".", color="c", ms=2)
ax.set_title(d["label"], fontsize=9)
ax.set_xticks([]); ax.set_yticks([])
plt.suptitle("YP channel (block means of 25 x 23 px); red: footprints found; cyan: ROI centres along bb'")
plt.tight_layout()
plt.show()
print("Display range per panel: 5th to 99.5th percentile of the non-zero block means.")
ov = {d["subject"]: d["overview"] for d in dishes}
f1f = ov["sub-Fig1F"]
print("sub-Fig1F: channel 0 identical to channel 1:", bool(np.array_equal(f1f[0], f1f[1])))
Display range per panel: 5th to 99.5th percentile of the non-zero block means. sub-Fig1F: channel 0 identical to channel 1: True
# Duplicated recordings: identical overviews of either channel across files.
subjects = sorted(ov)
dups = []
for i, a in enumerate(subjects):
for b in subjects[i + 1:]:
for ca in (bp.MB, bp.YP):
for cb in (bp.MB, bp.YP):
if np.array_equal(ov[a][ca], ov[b][cb]):
dups.append((a, ["MB", "YP"][ca], b, ["MB", "YP"][cb]))
print("Identical channel overviews across files:", dups)
# Which file each YP image belongs to: phase correlation of the fine structure of a file's MemBrite
# and YP channels (cells, debris), on the central 9.6 x 9.6 mm. Same scan: a sharp peak at zero shift.
from scipy.ndimage import gaussian_filter
def fine(x):
x = np.log1p(x[150:750, 150:810])
return x - gaussian_filter(x, 8)
def peak(a, b):
A, B = np.fft.fft2(fine(a)), np.fft.fft2(fine(b))
R = A * np.conj(B)
r = np.real(np.fft.ifft2(R / (np.abs(R) + 1e-9)))
return r[0, 0], np.unravel_index(np.argmax(r), r.shape)
rows = []
for s in subjects:
if s == "sub-Fig1F":
continue
p, where = peak(ov[s][bp.MB], ov[s][bp.YP])
rows.append({"subject": s, "MB_vs_own_YP_peak": round(p, 3), "argmax_at_zero_shift": where == (0, 0)})
reg = pd.DataFrame(rows)
print(reg.to_string(index=False))
others = [peak(ov[a][bp.MB], ov[b][bp.YP])[0] for a in subjects for b in subjects
if a != b and "Fig1F" not in a + b][:60]
print(f"Baseline, MB of one dish against YP of another (60 pairs): {np.mean(others):.3f} "
f"(range {np.min(others):.3f} to {np.max(others):.3f})")
print("MB of sub-Fig2-25-04 against YP of sub-Fig2-0-03:",
round(peak(ov["sub-Fig2-25-04"][bp.MB], ov["sub-Fig2-0-03"][bp.YP])[0], 3))
Identical channel overviews across files: [('sub-Fig2-0-03', 'YP', 'sub-Fig2-25-04', 'YP')]
subject MB_vs_own_YP_peak argmax_at_zero_shift sub-Fig2-0-03 0.028 True sub-Fig2-0-06 0.287 True sub-Fig2-0-07 0.321 True sub-Fig2-100-03 0.348 True sub-Fig2-100-07 0.328 True sub-Fig2-100-08 0.324 True sub-Fig2-25-04 0.454 True sub-Fig2-25-10 0.310 True sub-Fig2-25-14 0.333 True sub-Fig2-50-06 0.379 True sub-Fig2-50-10 0.355 True sub-Fig2-50-12 0.363 True sub-Fig2-50-13 0.349 True sub-Fig2-50-18 0.377 True sub-Fig2-75-02 0.373 True sub-Fig2-75-05 0.361 True sub-Fig2-75-10 0.335 True
Baseline, MB of one dish against YP of another (60 pairs): 0.018 (range 0.007 to 0.031) MB of sub-Fig2-25-04 against YP of sub-Fig2-0-03: 0.454
The YP channel of sub-Fig2-0-03 (unipolar) is identical to the YP channel of sub-Fig2-25-04
(25% bipolar), while their MemBrite channels differ. In every other file the MemBrite and YP
channels share fine structure (a sharp phase-correlation peak at zero shift); in sub-Fig2-0-03
they do not, at the level of two unrelated dishes, while in sub-Fig2-25-04 they do. The YP image
stored in sub-Fig2-0-03 is therefore that of dish 25-04, and the unipolar YP image of dish 0-03 is
not in the dandiset. Each recording is counted once: the main analysis leaves sub-Fig2-0-03 out,
so unipolar pulses have 2 dishes. The
sub-Fig1F file holds the same image in both channels.
# Other dandisets that name this paper (relation IsPublishedIn), read for their file lists and metadata.
SIBLINGS = {"000548": "0.230519.1825", "000549": "0.230519.2043", "000550": "0.230520.1609", "000448": "0.230421.1844"}
sib = {}
for ds, v in SIBLINGS.items():
m = bp.dandiset_metadata(v, ds)
rel = [r["url"] for r in m.get("relatedResource", []) if r.get("relation") == "dcite:IsPublishedIn"]
sib[ds] = bp.assets(ds, v)
print(f"{ds} version {v}: {m['name']!r}; {len(sib[ds])} files; IsPublishedIn {rel}")
if ds != "000448":
cond = pd.Series([a["path"].split("/")[0][4:].rsplit("-", 1)[0] for a in sib[ds]]).value_counts().sort_index()
print(" files per condition:", cond.to_dict())
desc = pd.DataFrame(bp.session_descriptions("000448", SIBLINGS["000448"]))
desc["pulse_shape"] = desc.session_description.str.extract(r"Pulse_shape: (\w+)")[0]
desc["cell"] = desc.session_description.str.extract(r"\((cell\d+)\)")[0]
print(" 000448 cells by pulse shape (each recorded forward and reverse):",
desc.groupby("pulse_shape").cell.nunique().to_dict(), "; files:", len(desc))
print(" 000448 session description of the first file:", desc.session_description.iloc[0])
# The 5-pulse dishes of 000548 against the Fig. 2 dishes of 000489 with the same dish number.
size489 = {a["path"].split("/")[0]: a["size"] for a in bp.assets()}
rows = []
for a in sib["000548"]:
s = a["path"].split("/")[0]
if "Uni5-" in s or "Bi5-" in s:
twin = f"sub-Fig2-{'0' if 'Uni5' in s else '50'}-{s.rsplit('-', 1)[1]}"
rows.append({"000548": s, "000489": twin, "bytes_000548": a["size"], "bytes_000489": size489.get(twin),
"difference": a["size"] - size489[twin] if twin in size489 else None})
print("\n5-pulse files of 000548 and the 000489 file with the same dish number:")
print(pd.DataFrame(rows).to_string(index=False))
# Dish 0-03 as deposited in 000548, read for this check only.
dish_0_03_fig3 = bp.Dish(next(a for a in sib["000548"] if a["path"].startswith("sub-Fig3-Uni5-03/")))
f3 = dish_0_03_fig3.overview()
print("\nsub-Fig3-Uni5-03 (000548) against sub-Fig2-0-03 (000489):")
print(" MemBrite overviews identical:", bool(np.array_equal(f3[bp.MB], ov["sub-Fig2-0-03"][bp.MB])),
"; YP overviews identical:", bool(np.array_equal(f3[bp.YP], ov["sub-Fig2-0-03"][bp.YP])))
p, where = peak(f3[bp.MB], f3[bp.YP])
print(f" MB against its own YP: peak {p:.3f} at shift {tuple(int(w) for w in where)} "
f"(000489's 0-03: {reg.set_index('subject').loc['sub-Fig2-0-03', 'MB_vs_own_YP_peak']:.3f})")
print(f" its YP against the YP of 000489's 0-03 (= 25-04): {peak(f3[bp.YP], ov['sub-Fig2-0-03'][bp.YP])[0]:.3f}")
print(" session start:", dish_0_03_fig3.meta["session_start"], "; 000489 0-03:",
next(d for d in dishes if d["subject"] == "sub-Fig2-0-03")["meta"]["session_start"])
print(" file session description:", dish_0_03_fig3.meta["session_description"])
fp = bp.find_footprints(f3[bp.YP])
print(" footprints found in its YP image: centres", fp["centres"].round().tolist(), "px apart",
f"{np.hypot(*(fp['centres'][1] - fp['centres'][0])) * bp.UM_PER_PX / 1000:.2f} mm, diameters",
(2 * fp["radii"] * bp.UM_PER_PX / 1000).round(2).tolist(), "mm (nominal 4.5 and 1.5 mm)")
000548 version 0.230519.1825: 'Effect of the number of pulses on electroporation by unipolar and 50 % bipolar pulses'; 19 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
files per condition: {'Fig3-Bi10': 1, 'Fig3-Bi15': 3, 'Fig3-Bi5': 5, 'Fig3-Uni10': 4, 'Fig3-Uni15': 3, 'Fig3-Uni5': 3}
000549 version 0.230519.2043: 'Effect of the pulse duration on electroporation by unipolar and 50% bipolar pulses'; 26 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
files per condition: {'Fig4-5-Bi100us': 3, 'Fig4-5-Bi1200ns': 3, 'Fig4-5-Bi300ns': 3, 'Fig4-5-Bi500us': 2, 'Fig4-5-Bi50us': 2, 'Fig4-5-Uni100us': 3, 'Fig4-5-Uni1200ns': 2, 'Fig4-5-Uni300ns': 3, 'Fig4-5-Uni500us': 2, 'Fig4-5-Uni50us': 3}
000550 version 0.230520.1609: 'Pulse repetition rate'; 17 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
files per condition: {'Fig6-5xBi-0,83MHz': 3, 'Fig6-5xBi-100kHz': 3, 'Fig6-5xBi-10kHz': 3, 'Fig6-5xUni-0,83MHz': 2, 'Fig6-5xUni-100kHz': 3, 'Fig6-5xUni-10kHz': 3}
000448 version 0.230421.1844: 'Time kinetics of the membrane potential at the cathode- and anode-facing poles of a cell induced by a train of 5 pulses at 833 kHz'; 18 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
000448 cells by pulse shape (each recorded forward and reverse): {'bipolar': 4, 'unipolar': 5} ; files: 18
000448 session description of the first file: Pulse_shape: bipolar, Recording: forward (cell09), Cell_line: CHO, Pulse_duration: 600ns, Number_of_pulses: 5p, Pulse_repetition_rate: 833kHz, Electric_field: 0.3kV/cm, Dye: FluoVolt, Recording_type: strobe imaging
5-pulse files of 000548 and the 000489 file with the same dish number:
000548 000489 bytes_000548 bytes_000489 difference
sub-Fig3-Bi5-06 sub-Fig2-50-06 1047439718 1047437078 2640
sub-Fig3-Bi5-10 sub-Fig2-50-10 1032098503 1032095863 2640
sub-Fig3-Bi5-12 sub-Fig2-50-12 1039265891 1039263251 2640
sub-Fig3-Bi5-13 sub-Fig2-50-13 1016157823 1016155183 2640
sub-Fig3-Bi5-18 sub-Fig2-50-18 1035530035 1035527395 2640
sub-Fig3-Uni5-03 sub-Fig2-0-03 1037476805 1021672026 15804779
sub-Fig3-Uni5-06 sub-Fig2-0-06 1022353847 1022351207 2640
sub-Fig3-Uni5-07 sub-Fig2-0-07 1035590084 1035587444 2640
sub-Fig3-Uni5-03 (000548) against sub-Fig2-0-03 (000489): MemBrite overviews identical: True ; YP overviews identical: False MB against its own YP: peak 0.301 at shift (0, 0) (000489's 0-03: 0.028) its YP against the YP of 000489's 0-03 (= 25-04): 0.022 session start: 2021-07-29T13:00:26-04:00 ; 000489 0-03: 2021-07-29T13:00:26-04:00 file session description: Cells were electroporated by a train of 5 600 ns unipolar pulses at 1 Hz.The electrodes were two parallel stainless-steel cylinders, 1.5 mm in diameter and 3 mm apart
footprints found in its YP image: centres [[10325.0, 6967.0], [10642.0, 12818.0]] px apart 3.81 mm, diameters [1.97, 0.71] mm (nominal 4.5 and 1.5 mm)
Four other dandisets name this paper: 000548 (pulse number, Fig. 3), 000549 (pulse duration, Figs. 4 and 5), 000550 (repetition rate, Fig. 6) and 000448 (strobe imaging, Fig. 7, with fewer cells than the paper's 18 unipolar and 17 bipolar). Each is a separate dandiset and a separate target of the registry, and the claims file does not name them as a source for this one, so the claims on Figs. 3 to 7 are not tested here (their notes say where the data are). The 5-pulse dishes of 000548 carry the dish numbers of the Fig. 2 unipolar and 50% dishes, and all but one are 2640 bytes larger than the 000489 file of the same dish. The exception is unipolar dish 03: its MemBrite channel in 000548 is identical to 000489's, and its YP channel registers with that MemBrite channel, so 000548 holds the YP image of dish 0-03 that 000489 lacks. It is not added to the analysis: it is outside this dandiset, and in it the footprint finder takes a cell-free patch next to the right electrode for the footprint (the distance and diameters printed above are off the nominal 4.5 and 1.5 mm), so the dish could not be measured as the others are.
geo = pd.DataFrame([{
"dish": d["label"], "date": d["meta"]["session_start"][:10],
"centre_distance_mm": d["measure"]["separation_px"] * bp.UM_PER_PX / 1000,
"footprint_diam_left_mm": 2 * d["footprints"]["radii"][0] * bp.UM_PER_PX / 1000,
"footprint_diam_right_mm": 2 * d["footprints"]["radii"][1] * bp.UM_PER_PX / 1000,
"axis_angle_deg": np.degrees(np.arctan2(*(d["footprints"]["centres"][1] - d["footprints"]["centres"][0]))) - 90,
"YP0_left": d["measure"]["footprints"][0][bp.YP], "YP0_right": d["measure"]["footprints"][1][bp.YP],
"MB0_left": d["measure"]["footprints"][0][bp.MB], "MB0_right": d["measure"]["footprints"][1][bp.MB],
} for d in dishes if "measure" in d])
print(geo.round(3).to_string(index=False))
print(f"\nAt {bp.UM_PER_PX} um per pixel: electrode centres {geo.centre_distance_mm.median():.2f} mm apart "
f"(median; {geo.centre_distance_mm.min():.2f} to {geo.centre_distance_mm.max():.2f}); nominal "
f"{bp.CENTRE_DISTANCE_MM} mm (1.5 mm diameter, 3 mm side to side). Footprint diameter (equal-area disc of "
f"the dark region) median {np.median(geo[['footprint_diam_left_mm', 'footprint_diam_right_mm']]):.2f} mm; "
f"electrode diameter 1.5 mm.")
print("Glass bottom diameter in the overview of sub-Fig2-50-10 (rows with signal, mm):",
round((ov["sub-Fig2-50-10"][bp.YP].max(axis=1) > 0).sum() * bp.BLOCK[0] * bp.UM_PER_PX / 1000, 1))
print("\nDates of the dishes by amplitude:")
print(pd.crosstab(geo.dish.str.split("%").str[0].astype(int), geo.date).to_string())
dish date centre_distance_mm footprint_diam_left_mm footprint_diam_right_mm axis_angle_deg YP0_left YP0_right MB0_left MB0_right 0%-03 2021-07-29 4.424 1.524 1.372 -97.861 278.259 261.126 204.294 211.784 0%-06 2021-07-29 4.574 1.542 1.469 -91.058 238.476 253.200 186.069 206.630 0%-07 2021-07-29 4.407 1.395 1.830 -97.622 282.863 365.376 205.171 208.163 100%-03 2021-08-24 4.668 1.462 1.612 -98.807 203.200 223.699 263.435 248.783 100%-07 2021-08-24 4.475 1.503 1.370 -97.216 215.394 203.637 236.847 248.715 100%-08 2021-08-24 4.490 1.491 1.322 -94.412 173.861 177.206 256.165 255.640 25%-04 2021-07-26 4.424 1.524 1.372 -97.861 278.259 261.126 292.414 348.203 25%-10 2021-07-29 4.572 1.559 1.644 -96.215 298.277 323.455 262.909 264.122 25%-14 2021-07-29 4.818 1.776 1.667 -99.244 271.609 322.510 238.430 227.290 50%-06 2021-07-26 4.693 1.502 1.625 -92.601 261.867 288.769 303.543 334.647 50%-10 2021-07-26 4.540 1.388 1.511 -96.329 221.971 228.593 279.720 277.547 50%-12 2021-07-26 4.469 1.445 1.427 -93.880 264.208 257.333 295.726 300.290 50%-13 2021-07-29 4.678 1.481 1.814 -94.555 287.848 308.233 244.877 234.085 50%-18 2021-07-29 4.900 1.590 1.845 -96.286 339.104 322.269 261.295 271.510 75%-02 2021-08-24 4.469 1.474 1.358 -95.310 226.254 235.476 242.875 255.433 75%-05 2021-08-24 4.444 1.409 1.357 -92.436 218.664 214.804 260.129 258.572 75%-10 2021-08-24 4.603 1.388 1.389 -92.769 223.664 235.276 305.821 267.073 At 0.65 um per pixel: electrode centres 4.54 mm apart (median; 4.41 to 4.90); nominal 4.5 mm (1.5 mm diameter, 3 mm side to side). Footprint diameter (equal-area disc of the dark region) median 1.49 mm; electrode diameter 1.5 mm. Glass bottom diameter in the overview of sub-Fig2-50-10 (rows with signal, mm): 14.5 Dates of the dishes by amplitude: date 2021-07-26 2021-07-29 2021-08-24 dish 0 0 3 0 25 1 2 0 50 3 2 0 75 0 0 3 100 0 0 3
The pixel size is not in the files. At 0.65 um per pixel (the 6.5 um pixel of the ORCA-Flash4 camera through the 10x objective, Methods 2.5) the distance between the footprint centres and the footprint diameter agree with the electrode geometry of Methods 2.2, so the scale is consistent with the data; the variant that sets the scale from each dish's electrode distance is shown with every claim. The dishes come from three days: all unipolar dishes from 2021-07-29, and all 75% and 100% dishes from 2021-08-24, a day with no unipolar dish.
# Field model (Methods 2.3): two cylinders in the plane, 5.75 kV on the first phase.
print(f"Model field at the centre: {bp.field_kv_cm(0, 0):.2f} kV/cm (paper, Fig. 2C: strongest field 15.1 kV/cm)")
d0 = next(d for d in dishes if d["subject"] == "sub-Fig2-50-10")
prof = pd.DataFrame([{"k": r["k"], "distance_mm": r["distance_mm"], "ROI_mean_field": r["field"],
"point_field": bp.field_kv_cm(0, r["distance_mm"])} for r in d0["measure"]["rois"] if r["k"] >= 0])
print(prof.round(2).to_string(index=False))
grad = [(bp.field_kv_cm(0, max(r - 0.1, 0)) - bp.field_kv_cm(0, r + 0.1)) / bp.field_kv_cm(0, r) for r in prof.distance_mm]
print(f"Largest field change across one 200 um ROI along bb': {max(grad) * 100:.1f}% (paper, Methods 2.5: <10%)")
T = {}
keep = [d for d in dishes if "measure" in d and d["subject"] != "sub-Fig2-0-03"]
T["main"] = bp.roi_table(keep)
T["top side"] = bp.roi_table(keep, side="top")
T["bottom side"] = bp.roi_table(keep, side="bottom")
T["scale from electrodes"] = bp.roi_table(keep, which="measure_scaled")
T["left footprint"] = bp.roi_table(keep, footprint="left")
T["right footprint"] = bp.roi_table(keep, footprint="right")
main = T["main"]
# The model's field at the centre is 15.38 kV/cm against the paper's 15.1: fields scaled to match it,
# which moves the ROI taken as nearest each named field (6.7, 8.7, 15.1 kV/cm).
T["field scaled to 15.1 at centre"] = main.assign(field=main.field * 15.1 / bp.field_kv_cm(0, 0))
print("\nYP index (Methods 2.5) per dish along bb', main reading (both sides averaged), j = ROI index from the centre:")
print(main.pivot_table(index="j", columns="dish", values="YP").round(0).to_string())
print("\nRange of the YP index over all dishes and ROI:", round(main.YP.min(), 1), "to", round(main.YP.max(), 1))
raw = pd.DataFrame([{"dish": d["label"], "k": r["k"], "YP_roi": r["YP_roi"], "MB_roi": r["MB_roi"]}
for d in keep for r in d["measure"]["rois"]])
print("Raw ROI means (camera counts, uint16): YP", round(raw.YP_roi.min()), "to", round(raw.YP_roi.max()),
"; MB", round(raw.MB_roi.min()), "to", round(raw.MB_roi.max()))
print("ROI whose MemBrite mean is more than twice the dish median (debris or bright objects):",
raw.groupby("dish").apply(lambda g: g[g.MB_roi > 2 * g.MB_roi.median()][["k", "MB_roi"]].round(0).values.tolist()).to_dict())
Model field at the centre: 15.38 kV/cm (paper, Fig. 2C: strongest field 15.1 kV/cm)
k distance_mm ROI_mean_field point_field
0 0.0 15.38 15.38
1 0.2 15.24 15.24
2 0.4 14.85 14.85
3 0.6 14.24 14.24
4 0.8 13.46 13.46
5 1.0 12.58 12.58
6 1.2 11.65 11.65
7 1.4 10.72 10.71
8 1.6 9.80 9.80
9 1.8 8.94 8.94
10 2.0 8.14 8.14
11 2.2 7.41 7.41
12 2.4 6.75 6.74
13 2.6 6.15 6.15
14 2.8 5.61 5.61
15 3.0 5.13 5.13
Largest field change across one 200 um ROI along bb': 9.4% (paper, Methods 2.5: <10%)
YP index (Methods 2.5) per dish along bb', main reading (both sides averaged), j = ROI index from the centre:
dish 0%-06 0%-07 100%-03 100%-07 100%-08 25%-04 25%-10 25%-14 50%-06 50%-10 50%-12 50%-13 50%-18 75%-02 75%-05 75%-10
j
0 727.0 335.0 682.0 788.0 791.0 75.0 789.0 359.0 153.0 305.0 263.0 189.0 118.0 192.0 167.0 322.0
1 640.0 500.0 504.0 863.0 916.0 90.0 622.0 603.0 197.0 308.0 186.0 132.0 170.0 226.0 213.0 279.0
2 785.0 357.0 433.0 725.0 744.0 94.0 483.0 601.0 213.0 251.0 222.0 170.0 157.0 187.0 181.0 333.0
3 731.0 325.0 349.0 539.0 531.0 77.0 532.0 422.0 162.0 233.0 186.0 73.0 180.0 163.0 154.0 301.0
4 526.0 212.0 248.0 601.0 518.0 54.0 372.0 428.0 151.0 177.0 141.0 105.0 144.0 156.0 161.0 219.0
5 656.0 163.0 203.0 486.0 414.0 69.0 328.0 239.0 132.0 236.0 133.0 139.0 74.0 166.0 98.0 185.0
6 325.0 73.0 213.0 281.0 369.0 69.0 262.0 310.0 99.0 130.0 160.0 65.0 88.0 108.0 89.0 145.0
7 194.0 89.0 127.0 199.0 243.0 66.0 248.0 203.0 80.0 80.0 131.0 77.0 88.0 105.0 72.0 122.0
8 115.0 50.0 154.0 145.0 146.0 57.0 237.0 205.0 62.0 68.0 101.0 52.0 57.0 60.0 80.0 106.0
9 83.0 12.0 114.0 124.0 91.0 40.0 108.0 137.0 67.0 35.0 68.0 36.0 64.0 51.0 73.0 99.0
10 55.0 11.0 41.0 133.0 87.0 51.0 68.0 95.0 49.0 26.0 43.0 35.0 42.0 32.0 46.0 85.0
11 58.0 -17.0 44.0 85.0 73.0 44.0 41.0 49.0 34.0 29.0 23.0 -8.0 34.0 38.0 39.0 53.0
12 11.0 -28.0 50.0 55.0 45.0 12.0 49.0 59.0 35.0 23.0 56.0 16.0 43.0 24.0 33.0 29.0
13 2.0 -29.0 47.0 25.0 49.0 37.0 50.0 28.0 26.0 28.0 34.0 2.0 33.0 45.0 18.0 39.0
14 6.0 -77.0 42.0 51.0 29.0 33.0 68.0 37.0 31.0 3.0 26.0 3.0 25.0 13.0 24.0 45.0
15 28.0 -57.0 35.0 46.0 13.0 25.0 91.0 -6.0 38.0 -15.0 35.0 10.0 28.0 10.0 21.0 34.0
Range of the YP index over all dishes and ROI: -77.0 to 916.1
Raw ROI means (camera counts, uint16): YP 216 to 1722 ; MB 187 to 1260
ROI whose MemBrite mean is more than twice the dish median (debris or bright objects): {'0%-06': [], '0%-07': [], '100%-03': [], '100%-07': [], '100%-08': [], '25%-04': [], '25%-10': [], '25%-14': [], '50%-06': [], '50%-10': [[14.0, 1260.0]], '50%-12': [], '50%-13': [], '50%-18': [], '75%-02': [], '75%-05': [], '75%-10': [[5.0, 611.0]]}
def means(t):
return (t.groupby(["amplitude", "j"]).agg(field=("field", "mean"), YP=("YP", "mean"),
se=("YP", "sem"), n=("YP", "size")).reset_index())
M = means(main)
fig, axes = plt.subplots(1, 5, figsize=(20, 3.6))
for ax, (amp, g) in zip(axes, M.groupby("amplitude")):
ax.errorbar(g.field, g.YP, g.se, fmt="o-", ms=3)
for dish, h in main[main.amplitude == amp].groupby("dish"):
ax.plot(h.field, h.YP, ".", ms=3, alpha=0.5, label=dish)
ax.set_title(f"{amp}% second phase (n = {g.n.max()})")
ax.set_xlabel("field (kV/cm)"); ax.legend(fontsize=6)
axes[0].set_ylabel("YP (a.u.)")
plt.tight_layout(); plt.show()
print("Mean YP (a.u.) per amplitude, by ROI field:")
print(M.pivot(index="j", columns="amplitude", values="YP").round(1).assign(
field=M.groupby("j").field.mean().round(2)).to_string())
print("All points are within the axes.")
Mean YP (a.u.) per amplitude, by ROI field: amplitude 0 25 50 75 100 field j 0 530.7 407.7 205.4 226.9 753.8 15.38 1 570.4 438.3 198.3 239.5 761.0 15.24 2 571.1 392.8 202.7 233.6 634.0 14.85 3 527.8 343.8 166.7 205.9 472.7 14.24 4 369.0 284.8 143.8 178.6 455.6 13.46 5 409.6 212.1 142.9 149.6 367.5 12.58 6 199.1 213.9 108.7 114.0 287.7 11.65 7 141.4 172.2 90.8 99.6 189.8 10.72 8 82.2 166.4 67.9 82.0 148.6 9.80 9 47.7 94.9 54.0 74.3 109.6 8.94 10 32.8 71.0 38.8 54.4 86.7 8.14 11 20.3 44.4 22.1 43.6 67.2 7.41 12 -8.6 39.7 34.5 29.0 50.0 6.75 13 -13.3 38.4 24.7 33.9 40.4 6.15 14 -35.6 46.2 17.6 27.2 40.5 5.61 15 -14.7 36.7 19.2 21.8 31.4 5.13 All points are within the axes.
Claims on data the dandiset does not hold (Figs. 1E, 3 to 7) are blocked and listed in the summary. The six claims on Fig. 2 follow, each under every reading that the claims file names. For the ROI placement, reading (a), the two ROI at the same distance on both sides of bb' averaged, decides the outcomes, as an assumption: Methods 2.3 and Fig. 1C draw bb' as one line through the centre, on which the field is symmetric, and the paper names no side. Reading (b), one side only, is two readings, top and bottom, and the paper gives no way to choose between them; both are shown, and an outcome that either changes is marked as resting on the assumption. The other variants of the data are the scale set from the electrode distance, each footprint alone for YP_0 and MB_0, and the model field scaled so that the centre is at the paper's 15.1 kV/cm.
def nearest_j(t, value):
f = t.groupby("j").field.mean()
return int((f - value).abs().idxmin())
def ttest(a, b, paired):
a, b = np.asarray(a, float), np.asarray(b, float)
if paired:
return stats.ttest_rel(a, b).pvalue
return stats.ttest_ind(a, b).pvalue
def dish_values(t, amp, j):
return t[(t.amplitude == amp) & (t.j == j)].set_index("dish").YP
def ratio_table(t):
"""Bipolar YP per dish over the unipolar mean at the same ROI (Fig. 2C legend: unipolar as 100%)."""
uni = t[t.amplitude == 0].groupby("j").YP.mean()
r = t[t.amplitude > 0].copy()
r["pct"] = 100 * r.YP / r.j.map(uni)
return r
def run_variants(fn):
rows = []
for name, t in T.items():
out = fn(t, verbose=False)
rows.append({"data": name, **{k: v for k, v in out.items() if not isinstance(v, (pd.DataFrame, dict))}})
print(pd.DataFrame(rows).to_string(index=False))
AMPS = [0, 25, 50, 75, 100]
fig2a-uptake-rises-with-field¶
For unipolar 600 ns pulses and for bipolar 600+600 ns pulses with every second phase amplitude, a train of 5 pulses at 1 Hz evoked YP uptake that increased with the local electric field strength.
Criterion: per amplitude, dish YP at the ROI with the strongest field against the ROI with the weakest field, two-sided t-test (a) paired across dishes and (b) unpaired; reproduced when YP is higher at the strongest field with p < 0.05 for all five amplitudes, partially for three or four; methods_underspecified when (a) and (b) differ. Spearman correlation of mean YP with field reported.
def claim_fig2a(t, verbose=True, jmin=None):
jmax = int(t.groupby("j").field.mean().idxmax())
if jmin is None:
jmin = int(t.groupby("j").field.mean().idxmin())
rows = []
m = means(t)
for amp in AMPS:
hi, lo = dish_values(t, amp, jmax), dish_values(t, amp, jmin)
g = m[m.amplitude == amp]
rows.append({"amplitude": amp, "n": len(hi), "YP_strongest": hi.mean(), "YP_weakest": lo.mean(),
"p_paired": ttest(hi, lo.loc[hi.index], True), "p_unpaired": ttest(hi, lo, False),
"spearman_rho": stats.spearmanr(g.field, g.YP)[0], "spearman_p": stats.spearmanr(g.field, g.YP)[1]})
r = pd.DataFrame(rows)
def outcome(col):
k = int(((r.YP_strongest > r.YP_weakest) & (r[col] < 0.05)).sum())
return k, "reproduced" if k == 5 else "partially_reproduced" if k >= 3 else "not_reproduced"
(ka, oa), (kb, ob) = outcome("p_paired"), outcome("p_unpaired")
if verbose:
print(f"Strongest field ROI j = {jmax}, weakest j = {jmin}")
print(r.round(4).to_string(index=False))
return {"amplitudes_paired": ka, "paired": oa, "amplitudes_unpaired": kb, "unpaired": ob,
"outcome": oa if oa == ob else "methods_underspecified"}
res = claim_fig2a(main)
print(res)
print("\nUnder every variant of the data:")
run_variants(claim_fig2a)
# How far the ROI extend is a choice (bipolar.N_ROI_SIDE): the weakest ROI taken nearer the centre.
F = main.groupby("j").field.mean()
print("\nWeakest ROI taken nearer the centre (main reading):")
print(pd.DataFrame([{"weakest_j": j, "distance_mm": j * bp.ROI_UM / 1000, "field": round(F[j], 2),
**claim_fig2a(main, verbose=False, jmin=j)} for j in (10, 12, 15)]).to_string(index=False))
Strongest field ROI j = 0, weakest j = 15
amplitude n YP_strongest YP_weakest p_paired p_unpaired spearman_rho spearman_p
0 2 530.7258 -14.6514 0.1747 0.1126 0.9824 0.0
25 3 407.6941 36.6996 0.1860 0.1512 0.9765 0.0
50 5 205.4304 19.2461 0.0106 0.0008 0.9853 0.0
75 3 226.9046 21.7559 0.0407 0.0135 0.9882 0.0
100 3 753.7553 31.3755 0.0030 0.0000 0.9941 0.0
{'amplitudes_paired': 3, 'paired': 'partially_reproduced', 'amplitudes_unpaired': 3, 'unpaired': 'partially_reproduced', 'outcome': 'partially_reproduced'}
Under every variant of the data:
data amplitudes_paired paired amplitudes_unpaired unpaired outcome
main 3 partially_reproduced 3 partially_reproduced partially_reproduced
top side 3 partially_reproduced 3 partially_reproduced partially_reproduced
bottom side 2 not_reproduced 3 partially_reproduced methods_underspecified
scale from electrodes 3 partially_reproduced 3 partially_reproduced partially_reproduced
left footprint 2 not_reproduced 3 partially_reproduced methods_underspecified
right footprint 3 partially_reproduced 3 partially_reproduced partially_reproduced
field scaled to 15.1 at centre 3 partially_reproduced 3 partially_reproduced partially_reproduced
Weakest ROI taken nearer the centre (main reading):
weakest_j distance_mm field amplitudes_paired paired amplitudes_unpaired unpaired outcome
10 2.0 8.14 3 partially_reproduced 3 partially_reproduced partially_reproduced
12 2.4 6.75 2 not_reproduced 3 partially_reproduced methods_underspecified
15 3.0 5.13 3 partially_reproduced 3 partially_reproduced partially_reproduced
Result: partially reproduced, resting on an assumption. Mean YP rises with field for every amplitude (Spearman rho 0.98 to 0.99 over the 16 ROI). By the criterion's test of the strongest against the weakest ROI, YP is higher at p < 0.05 for 50%, 75% and 100% under both readings (paired p 0.011, 0.041, 0.003; unpaired p 0.0008, 0.014, < 0.0001), but not for unipolar (2 dishes, paired p 0.17, unpaired p 0.11) or 25% (3 dishes, paired p 0.19, unpaired p 0.15): 3 of 5 amplitudes under both. The same holds on the top side, with the scale from the electrodes, with the right footprint and with the field scaled to 15.1 kV/cm at the centre. On the bottom side alone, or with the left footprint alone, the paired test passes for 2 amplitudes and the unpaired for 3, which the criterion would call methods_underspecified; the main reading averages both sides and both footprints. How far the ROI extend is a choice: with the weakest ROI at 2.0 mm (8.14 kV/cm) the outcome is the same, and at 2.4 mm (6.75 kV/cm, the weakest field the paper names) the paired test passes for 2 amplitudes and the unpaired for 3.
fig2a-vertical-scales¶
Although the panels of Fig. 2A look similar, their vertical scale goes from 1200 a.u. for unipolar pulses down to only 250 a.u. for bipolar pulses with a 75% second phase.
Criterion: reproduced when the largest mean plus s.e.m. over ROI is at most 1200 a.u. for unipolar and at most 250 a.u. for 75% bipolar pulses, and the largest unipolar mean exceeds 250 a.u. The files hold uint16 camera counts with conversion 1.0 and no stated rescaling; they are taken as the units the paper measured (an assumption: nothing in the files says how the paper's a.u. relate to them). The ratio of the two largest means is reported either way.
def claim_scales(t, verbose=True):
m = means(t)
u, s = m[m.amplitude == 0], m[m.amplitude == 75]
top_u, top_s = (u.YP + u.se).max(), (s.YP + s.se).max()
ok = top_u <= 1200 and top_s <= 250 and u.YP.max() > 250
out = {"max_mean+sem_unipolar": round(top_u, 1), "max_mean_unipolar": round(u.YP.max(), 1),
"max_mean+sem_75pct": round(top_s, 1), "max_mean_75pct": round(s.YP.max(), 1),
"ratio_of_max_means": round(u.YP.max() / s.YP.max(), 2),
"outcome": "reproduced" if ok else "not_reproduced"}
if verbose:
for amp in AMPS:
g = m[m.amplitude == amp]
print(f"{amp:>3}%: largest mean {g.YP.max():7.1f}, largest mean + s.e.m. {(g.YP + g.se).max():7.1f} a.u.")
return out
print(claim_scales(main))
print("\nUnder every variant of the data:")
run_variants(claim_scales)
0%: largest mean 571.1, largest mean + s.e.m. 785.4 a.u.
25%: largest mean 438.3, largest mean + s.e.m. 615.2 a.u.
50%: largest mean 205.4, largest mean + s.e.m. 239.9 a.u.
75%: largest mean 239.5, largest mean + s.e.m. 283.2 a.u.
100%: largest mean 761.0, largest mean + s.e.m. 890.4 a.u.
{'max_mean+sem_unipolar': np.float64(785.4), 'max_mean_unipolar': np.float64(571.1), 'max_mean+sem_75pct': np.float64(283.2), 'max_mean_75pct': np.float64(239.5), 'ratio_of_max_means': np.float64(2.38), 'outcome': 'not_reproduced'}
Under every variant of the data:
data max_mean+sem_unipolar max_mean_unipolar max_mean+sem_75pct max_mean_75pct ratio_of_max_means outcome
main 785.4 571.1 283.2 239.5 2.38 not_reproduced
top side 730.9 530.7 275.1 236.3 2.25 not_reproduced
bottom side 1010.1 725.7 342.8 269.4 2.69 not_reproduced
scale from electrodes 774.7 574.1 284.0 240.3 2.39 not_reproduced
left footprint 738.7 568.6 311.3 250.4 2.27 not_reproduced
right footprint 832.0 576.3 255.2 228.7 2.52 not_reproduced
field scaled to 15.1 at centre 785.4 571.1 283.2 239.5 2.38 not_reproduced
Result: not reproduced, resting on an assumption. Taking the files' camera counts as the paper's a.u., the largest unipolar mean is 571.1 a.u. (mean plus s.e.m. 785.4, within 1200, and above 250), but the 75% largest mean plus s.e.m. is 283.2 a.u., above 250 (largest mean 239.5). The 75% value exceeds 250 under every variant of the data (255.2 to 342.8). The ratio of the largest unipolar to the largest 75% mean is 2.38; the paper's axis limits differ by 1200/250.
fig2b-exponential-same-slope¶
YP uptake increased exponentially with the field strength, and the second phase did not change the slope of the exponential fits on a semi-log scale.
Criterion: mean YP per ROI fitted with YP = A exp(kE), leaving out ROI below a cutoff of 6, 7 or 8 kV/cm; fits (a) nonlinear least squares on linear YP and (b) linear regression of log YP on field. Exponential adequate when its residual sum of squares on linear YP is below a straight line's for at least four of five amplitudes. Slopes compared with the unipolar slope by (i) per-dish fits and an unpaired t-test and (ii) the fits to the means with their standard errors. Reproduced when no bipolar slope differs at p < 0.05 and the exponential is adequate; not reproduced when any slope differs; methods_underspecified when the readings disagree. Under (b), points with mean (or dish) YP at or below zero cannot be logged and are dropped; the count is printed.
def fit_exp(E, Y, how):
"""k, se(k), A, residual sum of squares on linear YP, points used."""
E, Y = np.asarray(E, float), np.asarray(Y, float)
if how == "nls":
p0 = np.polyfit(E[Y > 0], np.log(Y[Y > 0]), 1) if (Y > 0).sum() >= 2 else [0.3, 0]
try:
popt, pcov = optimize.curve_fit(lambda e, a, k: a * np.exp(k * e), E, Y,
p0=[np.exp(p0[1]), p0[0]], maxfev=20000)
except RuntimeError:
return np.nan, np.nan, np.nan, np.nan, len(E)
return popt[1], np.sqrt(pcov[1, 1]), popt[0], ((Y - popt[0] * np.exp(popt[1] * E)) ** 2).sum(), len(E)
ok = Y > 0
if ok.sum() < 3:
return np.nan, np.nan, np.nan, np.nan, int(ok.sum())
lr = stats.linregress(E[ok], np.log(Y[ok]))
A = np.exp(lr.intercept)
return lr.slope, lr.stderr, A, ((Y - A * np.exp(lr.slope * E)) ** 2).sum(), int(ok.sum())
def claim_fig2b(t, verbose=True):
m = means(t)
readings, details = [], []
for cut in (6, 7, 8):
mm = m[m.field >= cut]
tt = t[t.field >= cut]
for how in ("nls", "log"):
fits = {}
adequate = 0
for amp in AMPS:
g = mm[mm.amplitude == amp]
k, se, A, rss, used = fit_exp(g.field, g.YP, how)
lin = np.polyfit(g.field, g.YP, 1)
rss_lin = ((g.YP - np.polyval(lin, g.field)) ** 2).sum()
adequate += bool(rss < rss_lin)
per_dish = {dish: fit_exp(h.field, h.YP, how)[0] for dish, h in tt[tt.amplitude == amp].groupby("dish")}
fits[amp] = (k, se, used, len(g), per_dish)
details.append({"cutoff": cut, "fit": how, "amplitude": amp, "k": k, "se_k": se,
"k_ratio_to_unipolar": np.nan, "points_used": f"{used}/{len(g)}",
"rss_exp": rss, "rss_line": rss_lin,
"per_dish_k": ", ".join(f"{v:.3f}" for v in per_dish.values())})
ku, seu, nu, _, du = fits[0]
for test in ("per-dish", "means"):
differs = []
for amp in AMPS[1:]:
k, se, n, _, dd = fits[amp]
if test == "per-dish":
a = [v for v in dd.values() if np.isfinite(v)]; b = [v for v in du.values() if np.isfinite(v)]
p = stats.ttest_ind(a, b).pvalue if len(a) > 1 and len(b) > 1 else np.nan
else:
tstat = (k - ku) / np.hypot(se, seu)
p = 2 * stats.t.sf(abs(tstat), n + nu - 4)
differs.append((amp, p))
any_diff = any(p < 0.05 for _, p in differs)
# The criterion: reproduced only when no slope differs and the exponential is adequate.
outcome = "reproduced" if not any_diff and adequate >= 4 else "not_reproduced"
readings.append({"cutoff": cut, "fit": how, "test": test, "exp_better_than_line": adequate,
**{f"p_{a}": round(p, 4) for a, p in differs}, "slope_differs": any_diff,
"outcome": outcome})
R = pd.DataFrame(readings)
Dt = pd.DataFrame(details)
for (cut, how), g in Dt.groupby(["cutoff", "fit"]):
Dt.loc[g.index, "k_ratio_to_unipolar"] = g.k / g[g.amplitude == 0].k.iloc[0]
outs = set(R.outcome)
if verbose:
print(Dt.round(4).to_string(index=False))
print()
print(R.to_string(index=False))
return {"readings_slope_differs": f"{int(R.slope_differs.sum())}/{len(R)}",
"readings_exp_adequate": f"{int((R.exp_better_than_line >= 4).sum())}/{len(R)}",
"outcomes": "/".join(sorted(outs)),
"outcome": outs.pop() if len(outs) == 1 else "methods_underspecified"}
print(claim_fig2b(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2b)
cutoff fit amplitude k se_k k_ratio_to_unipolar points_used rss_exp rss_line per_dish_k
6 nls 0 0.2933 0.0375 1.0000 14/14 49642.6008 46568.4338 0.258, 0.394
6 nls 25 0.2247 0.0137 0.7661 14/14 6162.3232 11366.6358 0.099, 0.266, 0.213
6 nls 50 0.2052 0.0125 0.6994 14/14 1491.6475 1880.6859 0.186, 0.258, 0.179, 0.231, 0.164
6 nls 75 0.2004 0.0092 0.6831 14/14 1144.4085 2563.7553 0.194, 0.194, 0.209
6 nls 100 0.2900 0.0132 0.9885 14/14 9257.7498 79107.0826 0.323, 0.276, 0.289
6 log 0 0.4175 0.0288 1.0000 12/14 120177.1812 46568.4338 0.503, 0.497
6 log 25 0.2661 0.0158 0.6374 14/14 12885.3303 11366.6358 0.124, 0.303, 0.280
6 log 50 0.2389 0.0147 0.5721 14/14 2654.4084 1880.6859 0.210, 0.304, 0.212, 0.333, 0.174
6 log 75 0.2226 0.0095 0.5332 14/14 1885.3366 2563.7553 0.222, 0.216, 0.235
6 log 100 0.3108 0.0073 0.7444 14/14 12883.6076 79107.0826 0.282, 0.327, 0.326
7 nls 0 0.2809 0.0383 1.0000 12/12 42705.0999 31099.9485 0.245, 0.382
7 nls 25 0.2174 0.0146 0.7738 12/12 5221.5483 7658.9587 0.081, 0.262, 0.203
7 nls 50 0.2018 0.0144 0.7182 12/12 1421.8082 605.1427 0.180, 0.255, 0.178, 0.220, 0.164
7 nls 75 0.1939 0.0093 0.6902 12/12 850.3384 1595.4499 0.192, 0.186, 0.200
7 nls 100 0.2874 0.0148 1.0230 12/12 8955.2048 53524.3089 0.329, 0.270, 0.285
7 log 0 0.4175 0.0288 1.0000 12/12 118419.1096 31099.9485 0.358, 0.497
7 log 25 0.2504 0.0197 0.5996 12/12 8684.6857 7658.9587 0.081, 0.305, 0.249
7 log 50 0.2459 0.0192 0.5890 12/12 3109.2087 605.1427 0.201, 0.322, 0.230, 0.204, 0.182
7 log 75 0.2074 0.0072 0.4968 12/12 1065.6243 1595.4499 0.231, 0.188, 0.207
7 log 100 0.2996 0.0080 0.7175 12/12 9917.1748 53524.3089 0.301, 0.288, 0.319
8 nls 0 0.2744 0.0400 1.0000 11/11 40480.9597 23422.8067 0.239, 0.374
8 nls 25 0.2099 0.0140 0.7648 11/11 4043.6012 7286.2761 0.080, 0.255, 0.192
8 nls 50 0.1926 0.0128 0.7018 11/11 935.7607 576.6908 0.174, 0.251, 0.164, 0.197, 0.158
8 nls 75 0.1903 0.0098 0.6936 11/11 756.8031 1290.2527 0.188, 0.183, 0.196
8 nls 100 0.2860 0.0160 1.0425 11/11 8862.9459 41895.8414 0.329, 0.268, 0.283
8 log 0 0.3953 0.0315 1.0000 11/11 93686.3676 23422.8067 0.362, 0.497
8 log 25 0.2250 0.0166 0.5691 11/11 4656.3923 7286.2761 0.080, 0.271, 0.213
8 log 50 0.2170 0.0126 0.5489 11/11 1386.5423 576.6908 0.187, 0.326, 0.188, 0.204, 0.171
8 log 75 0.1995 0.0070 0.5048 11/11 840.6989 1290.2527 0.234, 0.180, 0.193
8 log 100 0.2956 0.0093 0.7478 11/11 9373.2869 41895.8414 0.291, 0.286, 0.320
cutoff fit test exp_better_than_line p_25 p_50 p_75 p_100 slope_differs outcome
6 nls per-dish 4 0.2000 0.0468 0.0894 0.6181 True not_reproduced
6 nls means 4 0.0985 0.0352 0.0241 0.9333 True not_reproduced
6 log per-dish 2 0.0357 0.0042 0.0000 0.0022 True not_reproduced
6 log means 2 0.0001 0.0000 0.0000 0.0016 True not_reproduced
7 nls per-dish 3 0.2218 0.0548 0.0996 0.7578 False not_reproduced
7 nls means 3 0.1367 0.0674 0.0391 0.8768 True not_reproduced
7 log per-dish 2 0.1215 0.0152 0.0273 0.0991 True not_reproduced
7 log means 2 0.0001 0.0001 0.0000 0.0008 True not_reproduced
8 nls per-dish 3 0.2135 0.0494 0.1026 0.8303 True not_reproduced
8 nls means 3 0.1453 0.0671 0.0562 0.7899 False not_reproduced
8 log per-dish 3 0.0724 0.0154 0.0256 0.0887 True not_reproduced
8 log means 3 0.0001 0.0001 0.0000 0.0071 True not_reproduced
{'readings_slope_differs': '10/12', 'readings_exp_adequate': '2/12', 'outcomes': 'not_reproduced', 'outcome': 'not_reproduced'}
Under every variant of the data:
data readings_slope_differs readings_exp_adequate outcomes outcome
main 10/12 2/12 not_reproduced not_reproduced
top side 9/12 10/12 not_reproduced/reproduced methods_underspecified
bottom side 6/12 0/12 not_reproduced not_reproduced
scale from electrodes 8/12 2/12 not_reproduced/reproduced methods_underspecified
left footprint 12/12 2/12 not_reproduced not_reproduced
right footprint 7/12 2/12 not_reproduced/reproduced methods_underspecified
field scaled to 15.1 at centre 9/12 2/12 not_reproduced not_reproduced
Result: not reproduced, resting on an assumption. The criterion calls the claim reproduced when no bipolar slope differs from the unipolar slope at p < 0.05 and the exponential beats a straight line for at least four of five amplitudes, and not reproduced when a slope differs. None of the 12 readings (cutoff 6, 7, 8 kV/cm; least squares or log-linear fit; per-dish or means test) meets both parts. In 10 a bipolar slope differs; in the other 2 (least squares, per-dish test at 7 kV/cm and means test at 8 kV/cm) no slope differs but the exponential beats a straight line for only 3 of 5 amplitudes. The exponential is adequate in 2 of the 12 readings. The log-linear fits put every bipolar slope below the unipolar one (ratios 0.50 to 0.75, all four bipolar slopes differing at p < 0.01 in the means test); the least-squares fits give ratios 0.68 to 1.04. Under the bottom side, the left footprint and the scaled field every reading also gives not reproduced; on the top side, with the scale from the electrodes and with the right footprint some readings give reproduced, so the outcome rests on the main reading of the ROI, the footprints and the scale.
fig2c-max-inhibition-50-75¶
The strongest inhibition of YP uptake, to about 20% of the uptake after unipolar pulses, was reached with a second phase of 50 to 75% of the first (p < 0.01).
Criterion: mean YP per amplitude as percent of the unipolar mean at each ROI from 8.7 to 15.1 kV/cm (the ROI nearest each named field and those between, opening comment). Reproduced when the lower of the 50% and 75% values is below the 25% and 100% values at every such field, the percent is within 18 to 22% under reading (a) the mean over those fields for the better of 50 and 75%, or (b) the lowest percent at any of them, and the 50 or 75% dish values differ by an unpaired two-sided t-test at p < 0.01 under (i) against unipolar or (ii) against the 25% and 100% dishes, on raw YP or on dish values divided by the unipolar mean. Partially reproduced when ordering and test hold but the percent is outside tolerance under every reading. Where the test is made is not said: it is run at every ROI of the range (holding when p < 0.01 at all of them) and, as a second reading, on each dish's mean over the range. At a single ROI, dividing every dish by the same unipolar mean leaves the t-test unchanged, so raw and normalised values differ only in the pooled reading.
def claim_fig2c_max(t, verbose=True):
j_hi, j_lo = nearest_j(t, 15.1), nearest_j(t, 8.7)
js = list(range(min(j_hi, j_lo), max(j_hi, j_lo) + 1))
m = means(t)
pv = m.pivot(index="j", columns="amplitude", values="YP").loc[js]
pct = 100 * pv.div(pv[0], axis=0)
best = 50 if pct[50].mean() <= pct[75].mean() else 75
order = bool(((pct[[50, 75]].min(axis=1) < pct[25]) & (pct[[50, 75]].min(axis=1) < pct[100])).all())
val_a = pct[best].mean()
val_b = pct[[50, 75]].min().min()
in_tol = {"a": 18 <= val_a <= 22, "b": 18 <= val_b <= 22}
uni = t[t.amplitude == 0].groupby("j").YP.mean()
tests, per_roi = [], []
F = t.groupby("j").field.mean()
for amp in (50, 75):
# (i) against unipolar, (ii) against 25% and 100%, at every ROI of the range
for reading, other in (("i", [0]), ("ii", [25, 100])):
pj = {j: ttest(dish_values(t, amp, j), pd.concat([dish_values(t, o, j) for o in other]), False) for j in js}
per_roi += [{"amplitude": amp, "comparison": reading, "j": j, "field": round(F[j], 2), "p": p}
for j, p in pj.items()]
ps = list(pj.values())
sub = t[t.j.isin(js)].copy()
sub["norm"] = sub.YP / sub.j.map(uni)
for col in ("YP", "norm"):
a = sub[sub.amplitude == amp].groupby("dish")[col].mean()
b = sub[sub.amplitude.isin(other)].groupby("dish")[col].mean()
tests.append({"amplitude": amp, "comparison": reading, "values": col,
"p_every_ROI_max": max(ps), "ROI_p<0.01": sum(p < 0.01 for p in ps),
"p_at_8.7": pj[j_lo], "p_at_15.1": pj[j_hi],
"p_pooled_dish_means": ttest(a, b, False)})
Tt = pd.DataFrame(tests)
# Where the test is made: at every ROI of the range, on each dish's mean over the range, at both
# named fields (8.7 and 15.1 kV/cm), or at one ROI of the range at least.
where = {"every ROI": bool((Tt.p_every_ROI_max < 0.01).any()),
"pooled": bool((Tt.p_pooled_dish_means < 0.01).any()),
"both named fields": bool(((Tt["p_at_8.7"] < 0.01) & (Tt["p_at_15.1"] < 0.01)).any()),
"one ROI or more": bool((Tt["ROI_p<0.01"] > 0).any())}
def outcome(test_ok):
if order and test_ok and (in_tol["a"] or in_tol["b"]):
return "reproduced"
if order and test_ok:
return "partially_reproduced"
return "not_reproduced"
outs = {k: outcome(v) for k, v in where.items()}
if verbose:
print(f"ROI used: j = {js} (fields {t.groupby('j').field.mean().loc[js].round(2).tolist()} kV/cm)")
print("Percent of the unipolar mean:")
print(pct.round(1).to_string())
print(f"Better of 50 and 75%: {best}%; (a) mean over the range {val_a:.1f}%; (b) lowest {val_b:.1f}%")
print("Ordering (lower of 50/75 below 25 and 100 at every ROI):", order)
print(Tt.round(4).to_string(index=False))
print("p per ROI (unpaired), 50 and 75% against (i) unipolar and (ii) 25 and 100%:")
print(pd.DataFrame(per_roi).pivot_table(index=["j", "field"], columns=["amplitude", "comparison"],
values="p").round(4).to_string())
return {"best": best, "pct_a": round(val_a, 1), "pct_b": round(val_b, 1), "ordering": order,
**{f"test_{k}": v for k, v in where.items()},
"outcome": " / ".join(f"{k}: {v}" for k, v in outs.items())}
print(claim_fig2c_max(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2c_max)
ROI used: j = [1, 2, 3, 4, 5, 6, 7, 8, 9] (fields [15.24, 14.85, 14.24, 13.46, 12.58, 11.65, 10.72, 9.8, 8.94] kV/cm)
Percent of the unipolar mean:
amplitude 0 25 50 75 100
j
1 100.0 76.8 34.8 42.0 133.4
2 100.0 68.8 35.5 40.9 111.0
3 100.0 65.1 31.6 39.0 89.6
4 100.0 77.2 39.0 48.4 123.5
5 100.0 51.8 34.9 36.5 89.7
6 100.0 107.4 54.6 57.3 144.5
7 100.0 121.8 64.2 70.4 134.2
8 100.0 202.5 82.7 99.8 180.8
9 100.0 199.0 113.2 155.7 229.9
Better of 50 and 75%: 50%; (a) mean over the range 54.5%; (b) lowest 31.6%
Ordering (lower of 50/75 below 25 and 100 at every ROI): True
amplitude comparison values p_every_ROI_max ROI_p<0.01 p_at_8.7 p_at_15.1 p_pooled_dish_means
50 i YP 0.7935 1 0.7935 0.0018 0.0401
50 i norm 0.7935 1 0.7935 0.0018 0.1300
50 ii YP 0.0721 0 0.0186 0.0163 0.0166
50 ii norm 0.0721 0 0.0186 0.0163 0.0107
75 i YP 0.9957 0 0.4670 0.0109 0.1815
75 i norm 0.9957 0 0.4670 0.0109 0.4074
75 ii YP 0.2492 0 0.2492 0.0820 0.0915
75 ii norm 0.2492 0 0.2492 0.0820 0.0831
p per ROI (unpaired), 50 and 75% against (i) unipolar and (ii) 25 and 100%:
amplitude 50 75
comparison i ii i ii
j field
1 15.24 0.0018 0.0163 0.0109 0.0820
2 14.85 0.0254 0.0195 0.1447 0.0990
3 14.24 0.0265 0.0187 0.1432 0.1127
4 13.46 0.0457 0.0321 0.2098 0.1492
5 12.58 0.1105 0.0721 0.2585 0.1706
6 11.65 0.2666 0.0172 0.4428 0.0644
7 10.72 0.1802 0.0243 0.4080 0.1045
8 9.80 0.5541 0.0126 0.9957 0.0859
9 8.94 0.7935 0.0186 0.4670 0.2492
{'best': 50, 'pct_a': np.float64(54.5), 'pct_b': np.float64(31.6), 'ordering': True, 'test_every ROI': False, 'test_pooled': False, 'test_both named fields': False, 'test_one ROI or more': True, 'outcome': 'every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced'}
Under every variant of the data:
data best pct_a pct_b ordering test_every ROI test_pooled test_both named fields test_one ROI or more outcome
main 50 54.5 31.6 True False False False True every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
top side 50 107.9 32.7 True False False False True every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
bottom side 50 46.1 24.3 True False False False True every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
scale from electrodes 50 50.1 29.8 True False False False True every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
left footprint 50 49.1 31.5 True False False False True every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
right footprint 50 63.2 31.7 True False True False True every ROI: not_reproduced / pooled: partially_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
field scaled to 15.1 at centre 50 52.9 31.6 True False False False True every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
Result: not reproduced, resting on an assumption. Over the ROI from 8.94 to 15.24 kV/cm the 50% second phase inhibits most, and the lower of the 50% and 75% values is below the 25% and 100% values at every ROI, so the ordering holds. The inhibition is weaker than the paper's: 54.5% of the unipolar mean on average over the range (reading a) and 31.6% at its lowest, at 14.24 kV/cm (reading b), both outside 18 to 22%, so the claim is at most partially reproduced. Where the p < 0.01 test is made is not given, and four readings are run. At every ROI of the range it fails for both amplitudes (largest p for 50%, 0.79 against unipolar and 0.072 against 25 and 100%; for 75%, 0.996 and 0.249); on dish means over the range it fails (smallest p 0.011, 50% against 25 and 100%, normalised values); at both named fields it fails (50% against unipolar, p 0.79 at 8.94 kV/cm and 0.0018 at 15.24 kV/cm). It holds only at one ROI or more: 50% against unipolar at 15.24 kV/cm (p 0.0018) is the only one of the 36 tests (2 amplitudes, 2 comparisons, 9 ROI) below 0.01, and that reading would give partially reproduced. At the ROI of strongest inhibition (14.24 kV/cm) p is 0.027 against unipolar and 0.019 against 25 and 100%. The outcome takes the paper's p < 0.01 to apply over the range of fields where it reports the inhibition, not to one ROI chosen from 36 tests; this is an assumption. With the right footprint alone the pooled test reaches p < 0.01 (partially reproduced); with the field scaled to 15.1 kV/cm at the centre the range starts at the centre ROI and the mean percent is 52.9%.
fig2c-inhibition-flat-above-8-7¶
Between 8.7 and 15.1 kV/cm the degree of inhibition by the second phase did not depend on the field strength.
Criterion (a claim of absence): bipolar YP of each dish divided by the unipolar mean at the same ROI; for each bipolar amplitude, the values at 8.7 against 15.1 kV/cm by a two-sided t-test at p < 0.05, (a) paired across dishes and (b) unpaired. Reproduced when no amplitude differs under both; not reproduced when one does under both; methods_underspecified when (a) and (b) disagree. Every pair of ROI in the range is shown as a variant, with the size of each difference.
def claim_fig2c_flat(t, verbose=True):
r = ratio_table(t)
j_hi, j_lo = nearest_j(t, 15.1), nearest_j(t, 8.7)
rows = []
for amp in (25, 50, 75, 100):
a = r[(r.amplitude == amp) & (r.j == j_lo)].set_index("dish").pct
b = r[(r.amplitude == amp) & (r.j == j_hi)].set_index("dish").pct
rows.append({"amplitude": amp, "pct_at_8.7": a.mean(), "pct_at_15.1": b.mean(),
"difference": a.mean() - b.mean(),
"p_paired": ttest(a, b.loc[a.index], True), "p_unpaired": ttest(a, b, False)})
R = pd.DataFrame(rows)
pa, pb = bool((R.p_paired < 0.05).any()), bool((R.p_unpaired < 0.05).any())
outcome = ("reproduced" if not pa and not pb else "not_reproduced" if pa and pb
else "methods_underspecified")
if verbose:
print(f"8.7 kV/cm: ROI j = {j_lo}; 15.1 kV/cm: ROI j = {j_hi}")
print(R.round(4).to_string(index=False))
js = list(range(min(j_hi, j_lo), max(j_hi, j_lo) + 1))
pairs = []
for amp in (25, 50, 75, 100):
for i, x in enumerate(js):
for y in js[i + 1:]:
a = r[(r.amplitude == amp) & (r.j == x)].set_index("dish").pct
b = r[(r.amplitude == amp) & (r.j == y)].set_index("dish").pct
pairs.append({"amplitude": amp, "paired_sig": ttest(a, b.loc[a.index], True) < 0.05,
"unpaired_sig": ttest(a, b, False) < 0.05})
P = pd.DataFrame(pairs)
print("Variant, every pair of ROI in the range: pairs with p < 0.05 per amplitude")
print(P.groupby("amplitude").agg(pairs=("paired_sig", "size"), paired=("paired_sig", "sum"),
unpaired=("unpaired_sig", "sum")).to_string())
return {"paired_any_sig": pa, "unpaired_any_sig": pb, "outcome": outcome}
print(claim_fig2c_flat(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2c_flat)
8.7 kV/cm: ROI j = 9; 15.1 kV/cm: ROI j = 1
amplitude pct_at_8.7 pct_at_15.1 difference p_paired p_unpaired
25 199.0029 76.8311 122.1718 0.0640 0.1435
50 113.1618 34.7623 78.3995 0.0124 0.0016
75 155.6884 41.9911 113.6974 0.0494 0.0175
100 229.8687 133.4092 96.4594 0.1142 0.0349
Variant, every pair of ROI in the range: pairs with p < 0.05 per amplitude
pairs paired unpaired
amplitude
25 36 2 0
50 36 23 18
75 36 15 11
100 36 4 10
{'paired_any_sig': True, 'unpaired_any_sig': True, 'outcome': 'not_reproduced'}
Under every variant of the data:
data paired_any_sig unpaired_any_sig outcome
main True True not_reproduced
top side True True not_reproduced
bottom side True True not_reproduced
scale from electrodes True True not_reproduced
left footprint True True not_reproduced
right footprint True True not_reproduced
field scaled to 15.1 at centre True True not_reproduced
Result: not reproduced. The percent of the unipolar mean rises from the ROI at 15.24 kV/cm to the ROI at 8.94 kV/cm for every amplitude (25%: 76.8 to 199.0; 50%: 34.8 to 113.2; 75%: 42.0 to 155.7; 100%: 133.4 to 229.9). The difference is significant for 50% and 75% under both the paired (p 0.012, 0.049) and the unpaired test (p 0.0016, 0.018), and for 100% unpaired (p 0.035). Over all 36 pairs of ROI in the range, 23 (paired) and 18 (unpaired) differ for 50%. Every variant of the data gives the same outcome.
fig2c-weaker-at-6-7¶
Inhibition was weaker at 6.7 kV/cm than at the higher field strengths, with p < 0.01 for the 50% bipolar pulses and p < 0.05 for the 100% bipolar pulses.
Criterion: bipolar YP per dish as percent of the unipolar mean at the same ROI. For 50% and 100%, the ROI nearest 6.7 kV/cm against the higher fields (ROI from 8.7 to 15.1 kV/cm), (i) against each higher ROI or (ii) against the higher ROI pooled, either sufficing; each (a) paired and (b) unpaired, methods_underspecified when they differ. Reproduced when the percent at 6.7 kV/cm is higher than at every higher field, with p < 0.01 for 50% and p < 0.05 for 100%; partially reproduced when it is higher at every field but the tests hold for one amplitude only. The criterion divides by the unipolar mean at the same ROI. Where that mean is not above background (here: not more than twice its s.e.m. above zero), the percent is a ratio to background, not a measure of inhibition, and the claim cannot be tested; the per-dish unipolar values are printed.
def claim_fig2c_67(t, verbose=True):
r = ratio_table(t)
j67, j_hi, j_lo = nearest_j(t, 6.7), nearest_j(t, 15.1), nearest_j(t, 8.7)
js = list(range(min(j_hi, j_lo), max(j_hi, j_lo) + 1))
uni = t[t.amplitude == 0].groupby("j").YP.mean()
rows, verdict = [], {}
for amp, thr in ((50, 0.01), (100, 0.05)):
x = r[(r.amplitude == amp) & (r.j == j67)].set_index("dish").pct
higher_every = all(x.mean() > r[(r.amplitude == amp) & (r.j == j)].pct.mean() for j in js)
for paired in (True, False):
p_each = []
for j in js:
y = r[(r.amplitude == amp) & (r.j == j)].set_index("dish").pct
p_each.append(ttest(x, y.loc[x.index], True) if paired else ttest(x, y, False))
pool = r[(r.amplitude == amp) & r.j.isin(js)]
if paired:
p_pool = ttest(x, pool.groupby("dish").pct.mean().loc[x.index], True)
else:
p_pool = ttest(x, pool.pct, False)
holds = higher_every and (max(p_each) < thr or p_pool < thr)
verdict[(amp, paired)] = (higher_every, holds)
rows.append({"amplitude": amp, "test": "paired" if paired else "unpaired",
"pct_at_6.7": x.mean(), "pct_higher_fields_mean": pool.pct.mean(),
"higher_than_every_higher_ROI": higher_every,
"p_each_max": max(p_each), f"each_p<thr": f"{sum(p < thr for p in p_each)}/{len(p_each)}",
"p_pooled": p_pool, "holds": holds})
R = pd.DataFrame(rows)
def outcome(paired):
h50, h100 = verdict[(50, paired)], verdict[(100, paired)]
if h50[1] and h100[1]:
return "reproduced"
if h50[0] and h100[0] and (h50[1] or h100[1]):
return "partially_reproduced"
return "not_reproduced"
oa, ob = outcome(True), outcome(False)
if verbose:
print(f"6.7 kV/cm: ROI j = {j67} ({t.groupby('j').field.mean().loc[j67]:.2f} kV/cm); higher fields j = {js}")
print(f"Unipolar mean YP at j = {j67}: {uni.loc[j67]:.1f}; at the higher ROI: {uni.loc[js].round(1).tolist()}")
print(R.round(4).to_string(index=False))
# The percent of the unipolar mean measures inhibition only where the unipolar mean is above
# background; here taken as more than twice its s.e.m. above zero.
u67 = dish_values(t, 0, j67)
background = bool(u67.mean() <= 2 * u67.sem())
return {"field_6.7_ROI": round(t.groupby("j").field.mean().loc[j67], 2), "uni_mean_6.7": round(u67.mean(), 1),
"uni_sem_6.7": round(u67.sem(), 1), "uni_at_background": background, "paired": oa, "unpaired": ob,
"outcome": "blocked (unipolar mean at background)" if background
else oa if oa == ob else "methods_underspecified"}
print(claim_fig2c_67(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2c_67)
# The denominator: unipolar YP per dish at the ROI nearest 6.7 kV/cm and the ROI either side of it.
rows = []
for name, t in T.items():
j67 = nearest_j(t, 6.7)
for j in (j67 - 1, j67, j67 + 1):
u = dish_values(t, 0, j)
rows.append({"data": name, "j": j, "field": round(t[t.j == j].field.mean(), 2),
**{d: round(v, 1) for d, v in u.items()}, "mean": round(u.mean(), 1), "sem": round(u.sem(), 1)})
print("\nUnipolar YP per dish (a.u.) around the ROI nearest 6.7 kV/cm (the middle row of each variant):")
print(pd.DataFrame(rows).to_string(index=False))
6.7 kV/cm: ROI j = 12 (6.75 kV/cm); higher fields j = [1, 2, 3, 4, 5, 6, 7, 8, 9]
Unipolar mean YP at j = 12: -8.6; at the higher ROI: [570.4, 571.1, 527.8, 369.0, 409.6, 199.1, 141.4, 82.2, 47.7]
amplitude test pct_at_6.7 pct_higher_fields_mean higher_than_every_higher_ROI p_each_max each_p<thr p_pooled holds
50 paired -399.1128 54.4785 False 0.0068 9/9 0.0061 False
50 unpaired -399.1128 54.4785 False 0.0008 9/9 0.0000 False
100 paired -578.7826 137.4000 False 0.0049 9/9 0.0028 False
100 unpaired -578.7826 137.4000 False 0.0001 9/9 0.0000 False
{'field_6.7_ROI': np.float64(6.75), 'uni_mean_6.7': np.float64(-8.6), 'uni_sem_6.7': np.float64(19.6), 'uni_at_background': True, 'paired': 'not_reproduced', 'unpaired': 'not_reproduced', 'outcome': 'blocked (unipolar mean at background)'}
Under every variant of the data:
data field_6.7_ROI uni_mean_6.7 uni_sem_6.7 uni_at_background paired unpaired outcome
main 6.75 -8.6 19.6 True not_reproduced not_reproduced blocked (unipolar mean at background)
top side 6.75 -32.1 35.9 True not_reproduced not_reproduced blocked (unipolar mean at background)
bottom side 6.75 14.8 3.3 False reproduced reproduced reproduced
scale from electrodes 6.75 -7.4 15.7 True not_reproduced not_reproduced blocked (unipolar mean at background)
left footprint 6.75 7.9 3.0 False reproduced reproduced reproduced
right footprint 6.75 -25.2 42.2 True not_reproduced not_reproduced blocked (unipolar mean at background)
field scaled to 15.1 at centre 6.63 -8.6 19.6 True not_reproduced not_reproduced blocked (unipolar mean at background)
Unipolar YP per dish (a.u.) around the ROI nearest 6.7 kV/cm (the middle row of each variant):
data j field 0%-06 0%-07 mean sem
main 11 7.41 58.0 -17.3 20.3 37.7
main 12 6.75 11.0 -28.2 -8.6 19.6
main 13 6.15 2.0 -28.7 -13.3 15.3
top side 11 7.41 51.5 -46.4 2.6 49.0
top side 12 6.75 3.7 -68.0 -32.1 35.9
top side 13 6.15 -7.4 -51.8 -29.6 22.2
bottom side 11 7.41 64.5 11.7 38.1 26.4
bottom side 12 6.75 18.2 11.5 14.8 3.3
bottom side 13 6.15 11.4 -5.5 3.0 8.5
scale from electrodes 11 7.41 40.9 -16.9 12.0 28.9
scale from electrodes 12 6.75 8.3 -23.1 -7.4 15.7
scale from electrodes 13 6.15 -2.5 -25.1 -13.8 11.3
left footprint 11 7.41 49.5 21.7 35.6 13.9
left footprint 12 6.75 4.9 10.9 7.9 3.0
left footprint 13 6.15 -3.6 10.4 3.4 7.0
right footprint 11 7.41 66.6 -56.4 5.1 61.5
right footprint 12 6.75 17.0 -67.4 -25.2 42.2
right footprint 13 6.15 7.6 -67.8 -30.1 37.7
field scaled to 15.1 at centre 11 7.28 58.0 -17.3 20.3 37.7
field scaled to 15.1 at centre 12 6.63 11.0 -28.2 -8.6 19.6
field scaled to 15.1 at centre 13 6.04 2.0 -28.7 -13.3 15.3
Result: blocked (other), under the main reading of the ROI and the footprints. At the ROI nearest 6.7 kV/cm (6.75 kV/cm) the unipolar mean YP is -8.6 a.u. with an s.e.m. of 19.6 (the two unipolar dishes give 11.0 and -28.2), not above background. The percent of the unipolar mean there (-399% for 50%, -579% for 100%) is a ratio to background, not a measure of inhibition, so the claim cannot be tested on these two dishes under the main reading. On the top side, with the right footprint, with the scale from the electrodes and with the field scaled to 15.1 kV/cm at the centre the unipolar mean is also at background. On the bottom side alone (14.8 a.u., s.e.m. 3.3) or with the left footprint alone (7.9 a.u., s.e.m. 3.0) it is more than twice its s.e.m. above zero and the claim is reproduced under both pairings, with a denominator below 3% of the largest unipolar mean of the main reading (571.1 a.u.).
The 600 ns exposure of Figs. 4B and 5B on the Fig. 2 dishes (no outcome)¶
The claims on Figs. 4 and 5 use the dishes of the pulse duration experiment, which are not in the dandiset, and are blocked. The Fig. 2 dishes with 0% and 50% second phase had the same exposure as the 600 ns pair of Fig. 4 (5 pulses at 1 Hz, 5.75 kV first phase, 50% second phase; Results 3.1 and 3.3), but they are other dishes. This section applies the measures of fig4ac-ns-bipolar-20-25pct (the ratio of the fits of bipolar and unipolar mean YP, over fields above 7 or 8 kV/cm, with exponential, 2nd degree polynomial or the better fit per curve) and of fig5-ns-symmetric-along-aa (ROI along aa', the two sides compared at each distance) to them, to show what part of those claims the dandiset could speak to. It decides no outcome. Which electrode was the anode is not in the files, so the sides are named left and right.
def fit_curve(E, Y, kind):
E, Y = np.asarray(E, float), np.asarray(Y, float)
if kind == "poly2":
p = np.polyfit(E, Y, 2)
return (lambda e: np.polyval(p, e)), ((Y - np.polyval(p, E)) ** 2).sum()
k, se, A, rss, used = fit_exp(E, Y, "nls")
return (lambda e: A * np.exp(k * e)), rss
rows = []
for cut in (7, 8):
g = M[(M.field >= cut) & M.amplitude.isin([0, 50])]
E = np.sort(g.groupby("j").field.mean().values)
fits = {}
for amp in (0, 50):
h = g[g.amplitude == amp]
fits[amp] = {kind: fit_curve(h.field, h.YP, kind) for kind in ("exp", "poly2")}
best = min(fits[amp].items(), key=lambda kv: kv[1][1])
fits[amp]["best"] = best[1]
for kind in ("exp", "poly2", "best"):
ratio = 100 * fits[50][kind][0](E) / fits[0][kind][0](E)
rows.append({"cutoff": cut, "fit": kind, "ROI": len(E), "ratio_min_%": ratio.min(),
"ratio_max_%": ratio.max(), "ratio_mean_%": ratio.mean(),
"inside_18_27.5_everywhere": bool(((ratio >= 18) & (ratio <= 27.5)).all())})
print("Ratio of the 50% bipolar fit to the unipolar fit, 600 ns, Fig. 2 dishes (fig4ac measure):")
print(pd.DataFrame(rows).round(1).to_string(index=False))
r = ratio_table(main)
for cut in (7, 8):
js = sorted(main[main.field >= cut].j.unique())
lo_j, hi_j = max(js), min(js)
a = r[(r.amplitude == 50) & (r.j == lo_j)].set_index("dish").pct
b = r[(r.amplitude == 50) & (r.j == hi_j)].set_index("dish").pct
print(f"cutoff {cut}: dish percent at the lowest included field ({main.groupby('j').field.mean()[lo_j]:.2f} kV/cm) "
f"{a.mean():.1f}, at the highest ({main.groupby('j').field.mean()[hi_j]:.2f}) {b.mean():.1f}; "
f"paired p = {ttest(a, b.loc[a.index], True):.4f}")
aa = []
for d in keep:
feet = d["measure"]["footprints"]
yp0, mb0 = np.mean([f[bp.YP] for f in feet]), np.mean([f[bp.MB] for f in feet])
for roi in d["measure"]["rois_aa"]:
aa.append({"dish": d["label"], "amplitude": d["amplitude"], "k": roi["k"], "field": roi["field"],
"YP": bp.yp_index(roi["YP_roi"], roi["MB_roi"], yp0, mb0)})
aa = pd.DataFrame(aa)
rows = []
for amp in (0, 50):
for j in range(1, bp.N_ROI_AA + 1):
L = aa[(aa.amplitude == amp) & (aa.k == -j)].set_index("dish").YP
R = aa[(aa.amplitude == amp) & (aa.k == j)].set_index("dish").YP
rows.append({"amplitude": amp, "distance_mm": j * bp.ROI_UM / 1000,
"field": aa[aa.k == j].field.mean(), "left": L.mean(), "right": R.mean(),
"left_over_right": L.mean() / R.mean(),
"p_paired": ttest(L, R.loc[L.index], True), "p_unpaired": ttest(L, R, False)})
S = pd.DataFrame(rows)
print("\nYP along aa', left against right side at the same distance (fig5 measure), 600 ns, Fig. 2 dishes:")
print(S.round(4).to_string(index=False))
print("Distances with p < 0.05, paired / unpaired:",
S.groupby("amplitude").apply(lambda g: (int((g.p_paired < 0.05).sum()), int((g.p_unpaired < 0.05).sum()))).to_dict())
print(f"Range of YP along aa': {aa.YP.min():.1f} to {aa.YP.max():.1f} a.u.; fields {aa.field.min():.2f} to {aa.field.max():.2f} kV/cm")
Ratio of the 50% bipolar fit to the unipolar fit, 600 ns, Fig. 2 dishes (fig4ac measure):
cutoff fit ROI ratio_min_% ratio_max_% ratio_mean_% inside_18_27.5_everywhere
7 exp 12 34.5 64.9 46.7 False
7 poly2 12 -457.4 150.6 13.3 False
7 best 12 -457.4 150.6 13.3 False
8 exp 11 34.4 62.2 45.3 False
8 poly2 11 34.8 1537.4 184.2 False
8 best 11 34.8 1537.4 184.2 False
cutoff 7: dish percent at the lowest included field (7.41 kV/cm) 108.8, at the highest (15.38) 38.7; paired p = 0.1531
cutoff 8: dish percent at the lowest included field (8.14 kV/cm) 118.6, at the highest (15.38) 38.7; paired p = 0.0099
YP along aa', left against right side at the same distance (fig5 measure), 600 ns, Fig. 2 dishes:
amplitude distance_mm field left right left_over_right p_paired p_unpaired
0 0.2 15.5151 611.8128 573.4728 1.0669 0.5259 0.7411
0 0.4 15.9449 593.1983 553.6832 1.0714 0.8637 0.8249
0 0.6 16.7167 479.6662 498.9566 0.9613 0.2406 0.6420
0 0.8 17.9322 745.5819 620.3965 1.2018 0.0920 0.1381
0 1.0 19.7822 625.4799 699.2402 0.8945 0.5009 0.7464
0 1.2 22.6386 817.5989 999.3741 0.8181 0.2119 0.1093
50 0.2 15.5151 203.2038 223.9233 0.9075 0.6084 0.7120
50 0.4 15.9449 270.4474 208.3476 1.2981 0.0822 0.3966
50 0.6 16.7167 275.9588 233.7003 1.1808 0.0293 0.5757
50 0.8 17.9322 278.9468 304.7569 0.9153 0.6915 0.6849
50 1.0 19.7822 315.0202 359.8747 0.8754 0.5335 0.6535
50 1.2 22.6386 414.1029 407.1447 1.0171 0.9249 0.9502
Distances with p < 0.05, paired / unpaired: {0: (0, 0), 50: (1, 0)}
Range of YP along aa': 37.8 to 1360.1 a.u.; fields 15.38 to 22.64 kV/cm
print(f"Notebook ran in {(time.time() - started) / 60:.1f} min")
Notebook ran in 11.6 min
Summary¶
A failure to reproduce here may be a failure of this attempt, not of the paper: see the reasoning and the differences from the paper's method.
| Claim | Outcome |
|---|---|
| fig2a-uptake-rises-with-field | partially reproduced (assumed) |
| fig2a-vertical-scales | not reproduced (assumed) |
| fig2b-exponential-same-slope | not reproduced (assumed) |
| fig2c-max-inhibition-50-75 | not reproduced (assumed) |
| fig2c-inhibition-flat-above-8-7 | not reproduced |
| fig2c-weaker-at-6-7 | blocked: other, unipolar mean at background at 6.7 kV/cm (main reading) |
| 15 claims on Figs. 3, 4, 5, 6 and 7 | blocked: other, data in dandisets 000548, 000549, 000550 and 000448 |
| fig1e-heating | blocked: data missing |
The claims on Figs. 4 and 5 are blocked, as their dishes are not in the dandiset. On the Fig. 2 dishes, which had the 600 ns exposure of Fig. 4, the measures of those claims give a ratio of the bipolar to the unipolar fit of 34.4 to 64.9% with exponential fits (fig4ac-ns-bipolar-20-25pct expects 18 to 27.5%). With polynomial fits the ratio crosses zero above 7 kV/cm (minimum -457.4%) and runs from 34.8 to 1537.4% above 8 kV/cm, and YP on the two sides of aa' differs at p < 0.05 at no distance under both pairings. These are other dishes than the paper's, and decide no outcome.
How the analysis differs from the paper's. The paper measured fluorescence in MetaMorph in ROI it placed by hand, and took the field of each ROI from a finite element model in Sim4Life. Here the electrode footprints are found automatically, 200 um ROI are tiled contiguously along bb' on both sides of the centre and averaged by distance, YP_0 and MB_0 are averaged over both footprints, the pixel size is taken as 0.65 um (camera pixel over the 10x objective), and the field comes from the closed-form solution for two cylinders in the plane, which gives 15.38 kV/cm at the centre against the paper's 15.1. The dish sub-Fig2-0-03 is left out because its YP image belongs to another dish, so unipolar pulses have 2 dishes, not 3. Each claim is shown under one side only, under a scale set from the electrode distance, under each footprint alone, and with the field scaled to the paper's 15.1 kV/cm at the centre.
What was found about the dandiset.
- It holds only the Fig. 2 experiment (17 dishes) and a Fig. 1F sample image. Figs. 1E and 3 to 7 have no data in it. Those of Figs. 3 to 7 are in dandisets 000548, 000549, 000550 and 000448, which name the same paper; nothing in 000489 points to them.
- The YP channel of
sub-Fig2-0-03(unipolar) is identical to that ofsub-Fig2-25-04(25%) and does not register with its own MemBrite channel; the unipolar YP image of dish 0-03 is missing. Dandiset 000548 holds it: its file for that dish has the same MemBrite channel and a YP channel that registers with it. - The
sub-Fig1Ffile holds the same image in both channels. - The image is stored as an ImageSeries of shape (22323, 22323, 2) with a rate of 2 Hz, so its first axis reads as time. There is no pixel size, no electrode position, no ROI, no field per ROI, no footprint values and no pulse voltage.
- The series description says 144 images of a well of a six-well plate; the paper says 216 images of a 35 mm dish. The three 25% files share the acquisition name
Fig2_25. - All unipolar dishes were imaged on 2021-07-29 and all 75% and 100% dishes on 2021-08-24, with no unipolar dish that day. The 100% dishes show more uptake than the unipolar dishes (761.0 against 571.1 a.u. at most), which the paper's Fig. 2C, where every bipolar pulse inhibits, does not show.
- Pixel scale and geometry are consistent with the paper: at 0.65 um per pixel the electrode centres are 4.54 mm apart (nominal 4.5) and the footprints 1.49 mm across (nominal 1.5).