OSCR

Volitional deep brain stimulation following brain-computer interface training for Parkinson’s disease

Code ↔ Paper

2 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 2 matches
  1. [1] § Results › BCI effects on motor performance ↔ analysis.ipynb, lines 1080–1155 · score 0.67 · tapping speed, post BCI, pre BCI, constant DBS
  2. [2] § Materials And Methods › Motor performance assessment ↔ analysis.ipynb, lines 1080–1155 · score 0.52 · inter tap interval, speed

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,181 lines · 38 KB · no license · 2 matches

  1. # %%
  2. import polars as pl
  3. import numpy as np
  4. import seaborn as sns
  5. import matplotlib.pyplot as plt
  6. import datetime
  7. import glob
  8. import utils
  9. plt.rcParams["font.family"] = "sans-serif"
  10. plt.rcParams["font.sans-serif"] = ["Arial Unicode MS"]
  11. plt.rcParams["font.size"] = 11
  12. # %%
  13. # Import statsmodels and ols function
  14. import statsmodels.api as sm
  15. from statsmodels.formula.api import ols
  16. # from statsmodels.regression.mixed_linear_model import MixedLM
  17. # import statsmodels.formula.api as smf
  18. # from statsmodels.tools.sm_exceptions import ConvergenceWarning
  19. from scipy.stats import ttest_ind, ttest_rel
  20. # %% [markdown]
  21. # ## Game data
  22. # %% [markdown]
  23. # ### BCI game performance
  24. # %%
  25. def get_plane_file_list(
  26. subj,
  27. data_dir="data/plane_game_data",
  28. extension="",
  29. ):
  30. plane_data_dir = f"{data_dir}/{subj}/{extension}"
  31. plane_data_file_list = glob.glob("Session_*.csv", root_dir=plane_data_dir)
  32. plane_data_file_list.sort()
  33. return plane_data_file_list, plane_data_dir
  34. def read_plane_data(plane_data_file):
  35. df_plane = pl.read_csv(
  36. plane_data_file,
  37. has_header=False,
  38. skip_rows=1,
  39. null_values="-",
  40. new_columns=["Timestamp_ticks", "x", "y", "score"],
  41. ).with_columns(x=-pl.col("x"))
  42. df_plane = df_plane[:-1] # remove last row (usually incomplete)
  43. return df_plane
  44. def get_date_from_plane_data(df_plane):
  45. tick = df_plane["Timestamp_ticks"][0]
  46. converted_ticks = datetime.datetime(1, 1, 1) + datetime.timedelta(
  47. microseconds=tick / 10
  48. )
  49. # timestamp = converted_ticks.timestamp()
  50. # converted_time = converted_ticks.strftime("%Y-%m-%d %H:%M:%S.%fZ")[:-3]
  51. date = converted_ticks.strftime("%Y-%m-%d")
  52. return date
  53. def get_score_per_block(
  54. df_plane,
  55. x_per_second=17.3,
  56. t_start=30,
  57. break_length=60,
  58. ending_length=10,
  59. NF_block_length=300,
  60. ):
  61. x_block1_start = (t_start) * x_per_second
  62. x_block1_end = (t_start + NF_block_length) * x_per_second
  63. x_block2_start = (t_start + NF_block_length + break_length) * x_per_second
  64. x_block2_end = (t_start + NF_block_length * 2 + break_length) * x_per_second
  65. x_block3_start = (t_start + NF_block_length * 2 + break_length) * x_per_second
  66. x_block3_end = (t_start + NF_block_length * 3 + break_length * 2) * x_per_second
  67. score_block1_start = df_plane.filter(pl.col("x") <= x_block1_start)["score"][-1]
  68. score_block1_end = df_plane.filter(pl.col("x") <= x_block1_end)["score"][-1]
  69. score_block2_start = df_plane.filter(pl.col("x") <= x_block2_start)["score"][-1]
  70. score_block2_end = df_plane.filter(pl.col("x") <= x_block2_end)["score"][-1]
  71. score_block3_start = df_plane.filter(pl.col("x") <= x_block3_start)["score"][-1]
  72. score_block3_end = df_plane.filter(pl.col("x") <= x_block3_end)["score"][-1]
  73. score_per_block = [
  74. score_block1_end - score_block1_start,
  75. score_block2_end - score_block2_start,
  76. score_block3_end - score_block3_start,
  77. ]
  78. return score_per_block
  79. def get_score_for_sessions(
  80. subj, first_session, last_session, file_list, plane_data_dir
  81. ):
  82. df_score_all = pl.DataFrame({})
  83. for session_num in range(first_session, last_session):
  84. plane_data_file = f"{plane_data_dir}/{file_list[session_num]}"
  85. # print(plane_data_file)
  86. df_plane = read_plane_data(plane_data_file)
  87. date = get_date_from_plane_data(df_plane)
  88. score_per_block = get_score_per_block(df_plane)
  89. # dbs condition
  90. dbs_condition = file_list[session_num][37:-4]
  91. if dbs_condition == "":
  92. dbs_condition = "constant"
  93. dict_game_performance = {
  94. "subj": subj,
  95. "date": date,
  96. "session": session_num,
  97. "dbs": dbs_condition,
  98. "block": [1, 2, 3],
  99. "score": score_per_block,
  100. }
  101. df_game_performance = pl.DataFrame(dict_game_performance)
  102. df_score_all = pl.concat((df_score_all, df_game_performance))
  103. return df_score_all
  104. # %%
  105. subj = "RCS05"
  106. plane_file_list1_rcs05, plane_data_dir1_rcs05 = get_plane_file_list(subj, extension="")
  107. plane_file_list2_rcs05, plane_data_dir2_rcs05 = get_plane_file_list(subj, extension="extended")
  108. df_score_training_rcs05 = get_score_for_sessions(
  109. subj, 0, 7, plane_file_list1_rcs05, plane_data_dir1_rcs05
  110. )
  111. df_score_testing_rcs05 = get_score_for_sessions(
  112. subj, 0, 12, plane_file_list2_rcs05, plane_data_dir2_rcs05
  113. )
  114. subj = "RCS15"
  115. plane_file_list1_rcs15, plane_data_dir1_rcs15 = get_plane_file_list(subj, extension="")
  116. plane_file_list2_rcs15, plane_data_dir2_rcs15 = get_plane_file_list(subj, extension="extended")
  117. df_score_training_rcs15 = get_score_for_sessions(
  118. subj, 0, 7, plane_file_list1_rcs15, plane_data_dir1_rcs15
  119. )
  120. df_score_testing_rcs15 = get_score_for_sessions(
  121. subj, 0, 12, plane_file_list2_rcs15, plane_data_dir2_rcs15
  122. )
  123. # combine from subjects
  124. df_score_training = pl.concat(
  125. (df_score_training_rcs05, df_score_training_rcs15)
  126. ).with_columns(
  127. session=pl.col("date").rank("dense").over("subj"),
  128. subj=pl.col("subj").replace({"RCS05": "patient1", "RCS15": "patient2"}),
  129. )
  130. df_score_testing = pl.concat(
  131. (df_score_testing_rcs05, df_score_testing_rcs15)
  132. ).with_columns(
  133. session=pl.col("date").rank("dense").over("subj"),
  134. subj=pl.col("subj").replace({"RCS05": "patient1", "RCS15": "patient2"}),
  135. )
  136. # %%
  137. # separate panels
  138. g = sns.catplot(
  139. df_score_training.to_pandas(),
  140. x="session",
  141. y="score",
  142. hue="subj",
  143. col="subj",
  144. alpha=0.7,
  145. kind="point",
  146. palette="Set2",
  147. height=2,
  148. aspect=1.5,
  149. )
  150. g.set_titles("{col_name}")
  151. g.fig.suptitle("training sessions", y=1.05)
  152. # plt.savefig("figures/training_session_score.png", dpi=300, bbox_inches="tight")
  153. # %%
  154. from statsmodels.formula.api import ols
  155. # Fit ordinary least squares linear regression
  156. model = ols("score ~ session", data=df_score_training.to_pandas()).fit()
  157. print("Linear regression for both patients combined:\n", model.summary(), "\n")
  158. print("Exact p-value for session coefficient: ", model.pvalues["session"])
  159. # %%
  160. # separate panels
  161. g = sns.FacetGrid(
  162. df_score_testing.to_pandas(),
  163. col="subj",
  164. hue="subj",
  165. palette="Set2",
  166. height=2.5,
  167. aspect=1,
  168. )
  169. g.map(
  170. sns.violinplot,
  171. "dbs",
  172. "score",
  173. alpha=0.2,
  174. width=0.5,
  175. inner=None,
  176. order=["constant", "increase", "decrease"],
  177. )
  178. g.map(
  179. sns.pointplot, "dbs", "score", alpha=0.7, order=["constant", "increase", "decrease"]
  180. )
  181. g.add_legend()
  182. g.set_titles("{col_name}")
  183. g.set_axis_labels("DBS policy", "score")
  184. g.fig.suptitle("testing sessions", y=1.05)
  185. # plt.savefig("figures/testing_session_score.png", dpi=300, bbox_inches="tight")
  186. # %%
  187. # ANOVA test
  188. for subj in ["patient1", "patient2"]:
  189. df_score_testing_subj = df_score_testing.filter(pl.col("subj") == subj)
  190. model = ols(formula="score ~ dbs", data=df_score_testing_subj.to_pandas()).fit()
  191. print(f"{subj}:\n", sm.stats.anova_lm(model, typ=2), "\n")
  192. # %% [markdown]
  193. # ### Get trial conditions
  194. # %%
  195. def get_x_intervals(t_intervals, block_num, x_per_second=17.3):
  196. x_intervals = np.array(t_intervals) * x_per_second
  197. # print(x_intervals)
  198. if block_num % 2 == 1:
  199. rest_intervals = np.array(
  200. [
  201. [x_intervals[i], x_intervals[i + 1]]
  202. for i in range(0, len(x_intervals) - 1, 2)
  203. ]
  204. )
  205. down_intervals = np.array(
  206. [
  207. [x_intervals[i], x_intervals[i + 1]]
  208. for i in range(1, len(x_intervals) - 1, 2)
  209. ]
  210. )
  211. else:
  212. down_intervals = np.array(
  213. [
  214. [x_intervals[i], x_intervals[i + 1]]
  215. for i in range(0, len(x_intervals) - 1, 2)
  216. ]
  217. )
  218. rest_intervals = np.array(
  219. [
  220. [x_intervals[i], x_intervals[i + 1]]
  221. for i in range(1, len(x_intervals) - 1, 2)
  222. ]
  223. )
  224. return x_intervals, rest_intervals, down_intervals
  225. x_per_second = 17.3
  226. # length in seconds
  227. t_start = 30
  228. trial_length = 15
  229. break_length = 60
  230. NF_block_length = 300
  231. t_intervals_block1 = [
  232. t_start + i * trial_length for i in range(int(NF_block_length / trial_length) + 1)
  233. ]
  234. t_intervals_block2 = [
  235. t_start + NF_block_length + break_length + i * trial_length
  236. for i in range(int(NF_block_length / trial_length) + 1)
  237. ]
  238. t_intervals_block3 = [
  239. t_start + NF_block_length * 2 + break_length * 2 + i * trial_length
  240. for i in range(int(NF_block_length / trial_length) + 1)
  241. ]
  242. x_intervals_block1 = get_x_intervals(t_intervals_block1, block_num=1)
  243. x_intervals_block2 = get_x_intervals(t_intervals_block2, block_num=2)
  244. x_intervals_block3 = get_x_intervals(t_intervals_block3, block_num=1)
  245. rest_intervals_all = np.concatenate(
  246. (x_intervals_block1[1], x_intervals_block2[1], x_intervals_block3[1]), axis=0
  247. )
  248. down_intervals_all = np.concatenate(
  249. (x_intervals_block1[2], x_intervals_block2[2], x_intervals_block3[2]), axis=0
  250. )
  251. trial_onsets_all = np.concatenate(
  252. (x_intervals_block1[0][:-1], x_intervals_block2[0][:-1], x_intervals_block3[0][:-1])
  253. )
  254. # %%
  255. def get_condition_for_x(
  256. x, rest_intervals=rest_intervals_all, down_intervals=down_intervals_all
  257. ):
  258. rest_test = np.int64(x >= rest_intervals)
  259. rest_result = np.sum(rest_test[:, 0] - rest_test[:, 1])
  260. down_test = np.int64(x >= down_intervals)
  261. down_result = np.sum(down_test[:, 0] - down_test[:, 1])
  262. if rest_result == 1:
  263. condition = "rest"
  264. elif down_result == 1:
  265. condition = "regulate"
  266. else:
  267. condition = "other"
  268. return condition
  269. def get_trial_num_for_x(x, trial_onsets=trial_onsets_all):
  270. return np.sum(x >= trial_onsets)
  271. def get_plane_data(
  272. subj, first_session, last_session, plane_data_file_list, plane_data_dir
  273. ):
  274. df_plane = pl.DataFrame({})
  275. for session_num in range(first_session, last_session):
  276. plane_data_file = f"{plane_data_dir}/{plane_data_file_list[session_num]}"
  277. df = read_plane_data(plane_data_file)
  278. date = get_date_from_plane_data(df)
  279. dbs_condition = plane_data_file_list[session_num][37:-4]
  280. if dbs_condition == "":
  281. dbs_condition = "constant"
  282. df = df.with_columns(
  283. subj=pl.lit(subj),
  284. session=pl.lit(session_num),
  285. date=pl.lit(date),
  286. dbs=pl.lit(dbs_condition),
  287. )
  288. df_plane = pl.concat((df_plane, df))
  289. timestamp_col = np.empty(len(df_plane))
  290. for i in range(len(df_plane)):
  291. ticks = df_plane["Timestamp_ticks"][i]
  292. converted_ticks = datetime.datetime(1, 1, 1) + datetime.timedelta(
  293. microseconds=ticks / 10
  294. )
  295. timestamp_col[i] = converted_ticks.timestamp()
  296. df_plane = df_plane.with_columns(unixtime=timestamp_col)
  297. return df_plane
  298. def get_condition_for_plane_data(df_plane):
  299. df_plane_condition = df_plane.with_columns(
  300. condition=pl.col("x").map_elements(get_condition_for_x, return_dtype=pl.String),
  301. trial=pl.col("x").map_elements(get_trial_num_for_x, return_dtype=pl.Int64),
  302. )
  303. df_plane_condition = df_plane_condition.filter(
  304. pl.col("condition").is_in(["regulate", "rest"])
  305. )
  306. return df_plane_condition
  307. # %%
  308. # RCS05
  309. df_plane_training_rcs05 = get_plane_data(
  310. "RCS05", 0, 7, plane_file_list1_rcs05, plane_data_dir1_rcs05
  311. )
  312. df_plane_training_rcs05_condition = get_condition_for_plane_data(
  313. df_plane_training_rcs05
  314. )
  315. # recalculate condition for RCS05 initial training sessions because the trial conditions were counterbalanced across blocks
  316. df_plane_training_rcs05_condition = df_plane_training_rcs05_condition.with_columns(
  317. condition=pl.when(pl.col("trial") % 2 == 0)
  318. .then(pl.lit("regulate"))
  319. .otherwise(pl.lit("rest"))
  320. )
  321. df_plane_testing_rcs05 = get_plane_data(
  322. "RCS05", 0, 12, plane_file_list2_rcs05, plane_data_dir2_rcs05
  323. )
  324. df_plane_testing_rcs05_condition = get_condition_for_plane_data(df_plane_testing_rcs05)
  325. # RCS15
  326. df_plane_training_rcs15 = get_plane_data(
  327. "RCS15", 0, 7, plane_file_list1_rcs15, plane_data_dir1_rcs15
  328. )
  329. df_plane_training_rcs15_condition = get_condition_for_plane_data(
  330. df_plane_training_rcs15
  331. )
  332. df_plane_testing_rcs15 = get_plane_data(
  333. "RCS15", 0, 12, plane_file_list2_rcs15, plane_data_dir2_rcs15
  334. )
  335. df_plane_testing_rcs15_condition = get_condition_for_plane_data(df_plane_testing_rcs15)
  336. # combine from subjects
  337. df_plane_condition_training = pl.concat(
  338. (df_plane_training_rcs05_condition, df_plane_training_rcs15_condition)
  339. ).with_columns(session=pl.col("date").rank("dense").over("subj"))
  340. df_plane_condition_testing = pl.concat(
  341. (df_plane_testing_rcs05_condition, df_plane_testing_rcs15_condition)
  342. ).with_columns(session=pl.col("date").rank("dense").over("subj"))
  343. # %% [markdown]
  344. # ## Neural data
  345. # %% [markdown]
  346. # ### Primary analyses
  347. # %%
  348. def get_rcs_file_list(
  349. subj,
  350. data_dir="data/neural_data",
  351. extension="",
  352. hemisphere="L",
  353. file_type="Adaptive_data",
  354. ):
  355. rcs_data_dir = f"{data_dir}/{subj}/{subj}{hemisphere}/{extension}"
  356. rcs_file_list = glob.glob(
  357. f"day*/Device*/{file_type}.parquet", root_dir=rcs_data_dir
  358. )
  359. rcs_file_list.sort()
  360. return rcs_file_list, rcs_data_dir
  361. def read_rcs_adaptive_parquet(file, data_dir):
  362. df_rcs = pl.read_parquet(f"{data_dir}/{file}", row_index_name="row_number")
  363. output_col = pl.Series(
  364. "output",
  365. [
  366. utils.uint_to_float(df_rcs["Ld0_output"][i], 32, 10)
  367. for i in range(len(df_rcs))
  368. ],
  369. )
  370. df_rcs = (
  371. df_rcs.with_columns(
  372. date=pl.col("localTime").cast(pl.String).str.slice(0, 10),
  373. unixtime=(pl.col("newDerivedTime") / 1e3),
  374. input=pl.col("featureInput"),
  375. output=output_col,
  376. )
  377. .with_columns(
  378. input_log=pl.col("input").log(),
  379. output_log=pl.col("output").log(),
  380. # height_on_game_screen = (pl.col('output').log()-3)*16.5
  381. )
  382. .filter(pl.col("row_number") > 0)
  383. )
  384. df_rcs = df_rcs.drop(["row_number", "newDerivedTime", "featureInput", "Ld0_output"])
  385. return df_rcs
  386. def get_adaptive_data_for_sessions(
  387. subj, first_session, last_session, parquet_list, data_dir
  388. ):
  389. df_rcs_all = pl.DataFrame()
  390. for file in parquet_list[first_session:last_session]:
  391. df_rcs = read_rcs_adaptive_parquet(file, data_dir)
  392. df_rcs = df_rcs.drop(["localTime"])
  393. df_rcs_all = pl.concat((df_rcs_all, df_rcs))
  394. df_rcs_all = df_rcs_all.insert_column(
  395. 0, pl.Series("subj", np.repeat(subj, len(df_rcs_all)))
  396. )
  397. df_rcs_all = df_rcs_all.sort("unixtime")
  398. return df_rcs_all
  399. # %%
  400. subj = "RCS05"
  401. adaptive_file_list1_rcs05, adaptive_data_dir1_rcs05 = get_rcs_file_list(
  402. subj, extension=""
  403. )
  404. adaptive_file_list2_rcs05, adaptive_data_dir2_rcs05 = get_rcs_file_list(
  405. subj, extension="extended"
  406. )
  407. df_rcs_adaptive_training_rcs05 = get_adaptive_data_for_sessions(
  408. subj, 0, 7, adaptive_file_list1_rcs05, adaptive_data_dir1_rcs05
  409. )
  410. df_rcs_adaptive_testing_rcs05 = get_adaptive_data_for_sessions(
  411. subj, 0, 12, adaptive_file_list2_rcs05, adaptive_data_dir2_rcs05
  412. )
  413. subj = "RCS15"
  414. adaptive_file_list1_rcs15, adaptive_data_dir1_rcs15 = get_rcs_file_list(
  415. subj, extension=""
  416. )
  417. adaptive_file_list2_rcs15, adaptive_data_dir2_rcs15 = get_rcs_file_list(
  418. subj, extension="extended"
  419. )
  420. df_rcs_adaptive_training_rcs15 = get_adaptive_data_for_sessions(
  421. subj, 0, 7, adaptive_file_list1_rcs15, adaptive_data_dir1_rcs15
  422. )
  423. df_rcs_adaptive_testing_rcs15 = get_adaptive_data_for_sessions(
  424. subj, 0, 12, adaptive_file_list2_rcs15, adaptive_data_dir2_rcs15
  425. )
  426. # combine from subjects
  427. df_rcs_adaptive_training = pl.concat(
  428. (df_rcs_adaptive_training_rcs05, df_rcs_adaptive_training_rcs15)
  429. ).with_columns(session=pl.col("date").rank("dense").over("subj"))
  430. df_rcs_adaptive_testing = pl.concat(
  431. (df_rcs_adaptive_testing_rcs05, df_rcs_adaptive_testing_rcs15)
  432. ).with_columns(session=pl.col("date").rank("dense").over("subj"))
  433. # %%
  434. join_tolerance = 1 # seconds
  435. thresh_rcs05 = 18
  436. thresh_rcs15 = 17
  437. df_rcs_adaptive_training_condition = df_rcs_adaptive_training.join_asof(
  438. df_plane_condition_training.select(
  439. ["subj", "unixtime", "condition", "dbs", "trial"]
  440. ).sort("unixtime"),
  441. on="unixtime",
  442. by="subj",
  443. tolerance=join_tolerance,
  444. ).with_columns(
  445. subj=pl.col("subj").replace({"RCS05": "patient1", "RCS15": "patient2"}),
  446. DBS_state=pl.col("CurrentAdaptiveState").replace(
  447. {"State 0": "low", "State 1": "high"}
  448. ),
  449. DBS_state_num=pl.col("CurrentAdaptiveState")
  450. .replace({"State 0": 0, "State 1": 1})
  451. .cast(pl.Int32),
  452. )
  453. df_rcs_adaptive_testing_condition = df_rcs_adaptive_testing.join_asof(
  454. df_plane_condition_testing.select(
  455. ["subj", "unixtime", "condition", "dbs", "trial"]
  456. ).sort("unixtime"),
  457. on="unixtime",
  458. by="subj",
  459. tolerance=join_tolerance,
  460. ).with_columns(
  461. subj=pl.col("subj").replace({"RCS05": "patient1", "RCS15": "patient2"}),
  462. DBS_state=pl.col("CurrentAdaptiveState").replace(
  463. {"State 0": "low", "State 1": "high"}
  464. ),
  465. DBS_state_num=pl.col("CurrentAdaptiveState")
  466. .replace({"State 0": 0, "State 1": 1})
  467. .cast(pl.Int32),
  468. )
  469. # %%
  470. # distribution
  471. df_plot = df_rcs_adaptive_testing_condition.filter(pl.col("trial") <= 40)
  472. g = sns.FacetGrid(
  473. df_plot.to_pandas(),
  474. col="subj",
  475. hue="condition",
  476. palette="Accent",
  477. hue_order=["regulate", "rest"],
  478. height=2,
  479. aspect=1.5,
  480. sharey=True,
  481. )
  482. g.map(sns.histplot, "output_log", alpha=0.6, binwidth=0.08)
  483. g.add_legend()
  484. g.set_axis_labels("normalized beta power")
  485. g.figure.suptitle("Distribution of beta power during test", y=1.05)
  486. g.set_titles("{col_name}")
  487. g.figure.axes[0].axvline(x=np.log(thresh_rcs05), color="black", linestyle="--")
  488. g.figure.axes[1].axvline(x=np.log(thresh_rcs15), color="black", linestyle="--")
  489. # plt.savefig(
  490. # "figures/beta_distribution_testing_session.png", dpi=300, bbox_inches="tight"
  491. # )
  492. # %%
  493. # Perform t-test to compare 'output_log' between 'regulate' and 'rest' for each subject
  494. for subj in ["patient1", "patient2"]:
  495. df_plot_subj = df_plot.filter(pl.col("subj") == subj)
  496. df_pd = df_plot_subj.to_pandas()
  497. group_reg = df_pd[df_pd["condition"] == "regulate"]["output_log"].dropna().values
  498. group_rest = df_pd[df_pd["condition"] == "rest"]["output_log"].dropna().values
  499. print(f"T-test for 'output_log' between 'regulate' and 'rest' for {subj}:")
  500. if len(group_reg) > 1 and len(group_rest) > 1:
  501. t_stat, p_val = ttest_ind(group_reg, group_rest, equal_var=False)
  502. d = utils.cohens_d(group_reg, group_rest)
  503. print(
  504. f" t({len(group_reg) + len(group_rest) - 2}): {t_stat:.3f}, p = {p_val:.3g}, Cohen's d = {d:.3f}"
  505. )
  506. else:
  507. print(" Not enough data for t-test")
  508. print()
  509. # %%
  510. df_plot = (
  511. df_rcs_adaptive_testing_condition.filter(pl.col("trial") <= 40)
  512. .groupby(["subj", "session", "dbs", "condition", "trial", "DBS_state"])
  513. .agg(pl.col("DBS_state").count().alias("count"))
  514. .with_columns(
  515. percentage=pl.col("count")
  516. / pl.col("count").sum().over(["subj", "session", "dbs", "condition", "trial"])
  517. * 100
  518. )
  519. )
  520. df_plot.sort(["subj", "session", "dbs", "condition", "trial", "DBS_state"])
  521. # # count plot
  522. # sns.catplot(data=df_rcs_adaptive_testing_condition.to_pandas(), x='condition', hue='DBS_state',
  523. # col="subj", kind="count", palette='Accent', order=['regulate','rest'], hue_order=['low', 'high'],
  524. # height=2, aspect=1)
  525. # percentage plot
  526. g = sns.catplot(
  527. data=df_plot.to_pandas(),
  528. x="condition",
  529. y="percentage",
  530. hue="DBS_state",
  531. col="subj",
  532. kind="bar",
  533. palette="Set2",
  534. order=["regulate", "rest"],
  535. hue_order=["low", "high"],
  536. height=2,
  537. aspect=1.2,
  538. ).set(ylim=[0, 120])
  539. plt.suptitle("Distribution of adaptive state during test", y=1.05)
  540. g._legend.set_title("beta state")
  541. g.set_titles("{col_name}")
  542. # plt.savefig(
  543. # "figures/adaptive_state_distribution_testing_session.png",
  544. # dpi=300,
  545. # bbox_inches="tight",
  546. # )
  547. # %%
  548. # ANOVA test
  549. for subj in ["patient1", "patient2"]:
  550. df_plot_subj = df_plot.filter(pl.col("subj") == subj)
  551. observed = df_plot_subj.to_pandas().pivot_table(
  552. index="DBS_state", columns="condition", values="percentage", aggfunc="mean"
  553. )
  554. print(f"\n{subj}:")
  555. print(observed)
  556. model = ols(
  557. formula="percentage ~ DBS_state*condition", data=df_plot_subj.to_pandas()
  558. ).fit()
  559. print(f"{subj}:\n", sm.stats.anova_lm(model, typ=2), "\n")
  560. # %%
  561. patient_list = ["patient1", "patient2"]
  562. rcs_thresholds = {"patient1": thresh_rcs05, "patient2": thresh_rcs15}
  563. stim_limits = {"patient1": [2.5, 3.5], "patient2": [3.5, 4.5]}
  564. for s in patient_list:
  565. if s == "patient1":
  566. df_plot_all = df_rcs_adaptive_testing_condition.filter(
  567. pl.col("subj") == "patient1",
  568. pl.col("session") == 1,
  569. pl.col("trial") >= 0,
  570. pl.col("trial") <= 8,
  571. )
  572. else:
  573. df_plot_all = df_rcs_adaptive_testing_condition.filter(
  574. pl.col("subj") == "patient2",
  575. pl.col("session") == 3,
  576. pl.col("trial") >= 10,
  577. pl.col("trial") <= 18,
  578. )
  579. policies = ["constant", "increase", "decrease"]
  580. x_labels = ["", "time (s)", ""]
  581. y_labels1 = ["beta power", "", ""]
  582. y_labels2 = ["state", "", ""]
  583. y_labels3 = ["stim", "", ""]
  584. rugplot_legends = [False, False, True]
  585. fig = plt.figure(figsize=(10, 2.5))
  586. grid = plt.GridSpec(5, 3, hspace=0.5, wspace=0.15)
  587. # plt.suptitle(f'{s}', y=1.1, fontsize=16)
  588. for i in range(len(policies)):
  589. policy = policies[i]
  590. df_plot = df_plot_all.filter(pl.col("dbs") == policy)
  591. df_plot = df_plot.with_columns(
  592. time=pl.col("unixtime") - df_plot["unixtime"].min()
  593. ).to_pandas()
  594. main_ax = fig.add_subplot(grid[:3, i : i + 1])
  595. mid_ax = fig.add_subplot(grid[3, i : i + 1])
  596. lower_ax = fig.add_subplot(grid[4, i : i + 1])
  597. # main panel
  598. main_ax.set(xticklabels=[]) # remove the tick labels
  599. main_ax.tick_params(bottom=False) # remove the ticks
  600. main_ax.axhline(y=rcs_thresholds[s], color="grey", lw=1, linestyle="--")
  601. main_ax.set_title(f"{policy} policy")
  602. sns.lineplot(
  603. ax=main_ax,
  604. data=df_plot,
  605. x="time",
  606. y="output",
  607. c="red",
  608. alpha=0.5,
  609. errorbar=None,
  610. estimator=None,
  611. ).set(ylabel=y_labels1[i], xlabel="", ylim=[-5, 100])
  612. # sns.rugplot(
  613. # ax=main_ax,
  614. # data=df_plot,
  615. # x="time",
  616. # hue="condition",
  617. # palette="Accent",
  618. # hue_order=["regulate", "rest"],
  619. # height=0.05,
  620. # lw=2,
  621. # expand_margins=True,
  622. # legend=rugplot_legends[i],
  623. # ).set(xlabel="")
  624. sns.lineplot(
  625. ax=main_ax,
  626. data=df_plot,
  627. x="time",
  628. y=0,
  629. hue="condition",
  630. units="trial",
  631. palette="Accent",
  632. hue_order=["regulate", "rest"],
  633. lw=2,
  634. legend=rugplot_legends[i],
  635. ).set(xlabel="")
  636. if i == 2:
  637. sns.move_legend(main_ax, "upper left", bbox_to_anchor=(1, 1))
  638. # mid panel
  639. mid_ax.set(xticklabels=[]) # remove the tick labels
  640. mid_ax.tick_params(bottom=False) # remove the ticks
  641. sns.lineplot(
  642. ax=mid_ax,
  643. data=df_plot,
  644. x="time",
  645. y="DBS_state_num",
  646. c="orange",
  647. alpha=0.5,
  648. errorbar=None,
  649. estimator=None,
  650. ).set(ylabel=y_labels2[i], xlabel="", ylim=[-0.5, 1.5])
  651. # lower panel
  652. sns.lineplot(
  653. ax=lower_ax,
  654. data=df_plot,
  655. x="time",
  656. y="stim_level",
  657. c="blue",
  658. alpha=0.5,
  659. errorbar=None,
  660. estimator=None,
  661. ).set(ylim=stim_limits[s], ylabel=y_labels3[i], xlabel=x_labels[i])
  662. # sns.rugplot(ax=lower_ax, data=df_plot, x="time", hue="condition", height=0.03, legend=False)
  663. # plt.savefig(f"figures/adaptive_DBS_dynamics_{s}.png", dpi=300, bbox_inches="tight")
  664. # %%
  665. df_plot = (
  666. df_rcs_adaptive_testing_condition.filter(pl.col("session") <= 4)
  667. .group_by("subj", "date", "session", "trial", "condition", "dbs")
  668. .agg(pl.col("stim_level").mean())
  669. .sort("session", "trial")
  670. )
  671. # sns.catplot(data=df_plot.to_pandas(), x='condition', y='stim_level', hue='dbs',
  672. # row="subj", kind="point", palette='Accent', order=['rest','regulate'], sharey=False,
  673. # height=2, aspect=1.5, markersize=3).set(ylabel='stim level')
  674. g = sns.FacetGrid(
  675. df_plot.to_pandas(),
  676. row="subj",
  677. hue="dbs",
  678. palette="Set2",
  679. hue_order=["constant", "increase", "decrease"],
  680. row_order=["patient1", "patient2"],
  681. height=2.5,
  682. aspect=1.5,
  683. sharey=False,
  684. )
  685. g.map(sns.violinplot, "condition", "stim_level", alpha=0.3, width=0.5, inner=None)
  686. g.map(sns.pointplot, "condition", "stim_level", markersize=3)
  687. g.add_legend(title="DBS policy")
  688. g.set_axis_labels("condition", "stim amplitude (mA)")
  689. g.figure.suptitle("Average stimulation level during test", y=1.05)
  690. g.set_titles("{row_name}")
  691. # plt.savefig(f'figures/average_stim_level_testing_session.png', dpi=300, bbox_inches='tight')
  692. # %%
  693. # ANOVA test
  694. for subj in ["patient1", "patient2"]:
  695. df_plot_subj = df_plot.filter(pl.col("subj") == subj)
  696. model = ols(
  697. formula="stim_level ~ dbs*condition", data=df_plot_subj.to_pandas()
  698. ).fit()
  699. print(f"{subj}:\n", sm.stats.anova_lm(model, typ=2), "\n")
  700. # from scipy.stats import ttest_ind
  701. for subj in ["patient1", "patient2"]:
  702. df_plot_subj = df_plot.filter(pl.col("subj") == subj)
  703. for dbs_cond in ["increase", "decrease"]:
  704. rest = df_plot_subj.filter(
  705. (pl.col("dbs") == dbs_cond) & (pl.col("condition") == "rest")
  706. )["stim_level"].to_numpy()
  707. regulate = df_plot_subj.filter(
  708. (pl.col("dbs") == dbs_cond) & (pl.col("condition") == "regulate")
  709. )["stim_level"].to_numpy()
  710. if len(rest) > 0 and len(regulate) > 0:
  711. t_stat, p_value = ttest_ind(rest, regulate, equal_var=True)
  712. # Calculate Cohen's d for independent samples
  713. n1 = len(rest)
  714. n2 = len(regulate)
  715. df_val = n1 + n2 - 2
  716. mean_rest = rest.mean()
  717. mean_regulate = regulate.mean()
  718. pooled_std = np.sqrt(((n1 - 1) * np.var(rest, ddof=1) + (n2 - 1) * np.var(regulate, ddof=1)) / df_val)
  719. cohens_d = (mean_regulate - mean_rest) / pooled_std if pooled_std > 0 else np.nan
  720. print(
  721. f"{subj}, dbs={dbs_cond}: t-test rest vs regulate: t = {t_stat:.3f}, p = {p_value}, df = {df_val}, cohen's d = {cohens_d:.3f}"
  722. )
  723. else:
  724. print(f"{subj}, dbs={dbs_cond}: Not enough data for t-test")
  725. # %% [markdown]
  726. # ### Supplementary analyses
  727. # %%
  728. df_plot = (
  729. df_rcs_adaptive_testing_condition.group_by(
  730. "subj", "session", "dbs", "condition", "trial"
  731. )
  732. .agg(pl.col("input_log").mean())
  733. .filter(pl.col("trial") <= 40)
  734. )
  735. # with violin plot
  736. g = sns.FacetGrid(
  737. df_plot.to_pandas(),
  738. col="subj",
  739. hue="condition",
  740. palette="Accent",
  741. height=2,
  742. aspect=1.4,
  743. hue_order=["regulate", "rest"],
  744. sharey=False,
  745. )
  746. g.map(
  747. sns.violinplot,
  748. "dbs",
  749. "input_log",
  750. alpha=0.3,
  751. width=0.5,
  752. inner=None,
  753. order=["constant", "increase", "decrease"],
  754. )
  755. g.map(sns.pointplot, "dbs", "input_log", alpha=1, markersize=3, capsize=0.1, lw=1)
  756. g.add_legend()
  757. g.set_titles("{col_name}")
  758. g.set_axis_labels("DBS policy", "beta power (log)")
  759. g.fig.suptitle("Cortical beta power during testing sessions", y=1.05)
  760. plt.savefig("figures/testing_session_beta_power.png", dpi=300, bbox_inches="tight")
  761. # %%
  762. # ANOVA test
  763. for subj in ["patient1", "patient2"]:
  764. df_plot_subj = df_plot.filter(pl.col("subj") == subj)
  765. model = ols(
  766. formula="input_log ~ dbs*condition", data=df_plot_subj.to_pandas()
  767. ).fit()
  768. print(f"{subj}:\n", sm.stats.anova_lm(model, typ=2), "\n")
  769. # %%
  770. # posthoc t-test comparison between regulate and rest under each DBS policy, print out t-test result and Cohen's d
  771. for subj in ["patient1", "patient2"]:
  772. df_plot_subj = df_plot.filter(
  773. pl.col("subj") == subj, pl.col("input_log").is_not_null()
  774. )
  775. df_pd = df_plot_subj.to_pandas()
  776. print(f"Posthoc t-test between regulate and rest for {subj}:")
  777. for dbs_policy in ["constant", "increase", "decrease"]:
  778. group_reg = (
  779. df_pd[(df_pd["dbs"] == dbs_policy) & (df_pd["condition"] == "regulate")][
  780. "input_log"
  781. ]
  782. .dropna()
  783. .values
  784. )
  785. group_rest = (
  786. df_pd[(df_pd["dbs"] == dbs_policy) & (df_pd["condition"] == "rest")][
  787. "input_log"
  788. ]
  789. .dropna()
  790. .values
  791. )
  792. if len(group_reg) > 1 and len(group_rest) > 1:
  793. t_stat, p_val = ttest_ind(group_reg, group_rest, equal_var=False)
  794. d = utils.cohens_d(group_reg, group_rest)
  795. print(f" DBS: {dbs_policy}")
  796. print(
  797. f" t({len(group_reg) + len(group_rest) - 2}): {t_stat:.3f}, p = {p_val:.3g}, Cohen's d = {d:.3f}"
  798. )
  799. else:
  800. print(f" DBS: {dbs_policy}")
  801. print(" Not enough data for t-test")
  802. print("")
  803. # %%
  804. # For each subj, session, trial: time from trial onset to first DBS_state == 'low'
  805. first_low_times = (
  806. df_rcs_adaptive_testing_condition.filter(pl.col("DBS_state") == "low")
  807. .group_by(["subj", "session", "trial", "condition", "dbs"])
  808. .agg(first_low_time=pl.col("unixtime").min())
  809. )
  810. # Merge with trial onset
  811. trial_onsets = df_rcs_adaptive_testing_condition.group_by(
  812. ["subj", "session", "trial", "condition", "dbs"]
  813. ).agg(trial_onset=pl.col("unixtime").min())
  814. # Join to compute time-to-first-low
  815. time_to_first_low = (
  816. first_low_times.join(
  817. trial_onsets, on=["subj", "session", "trial", "condition", "dbs"]
  818. )
  819. .with_columns(
  820. time_from_onset_to_first_low=pl.col("first_low_time") - pl.col("trial_onset")
  821. )
  822. .select(
  823. ["subj", "session", "trial", "condition", "dbs", "time_from_onset_to_first_low"]
  824. )
  825. )
  826. # %%
  827. df_plot = time_to_first_low.sort("subj", "session", "trial").filter(
  828. pl.col("trial") <= 40
  829. )
  830. g = sns.FacetGrid(
  831. df_plot.to_pandas(),
  832. col="subj",
  833. hue="condition",
  834. palette="Accent",
  835. height=2,
  836. aspect=1.4,
  837. hue_order=["regulate", "rest"],
  838. sharey=False,
  839. )
  840. # g.map(sns.violinplot, "dbs", "time_from_onset_to_first_low", alpha=0.3, width=0.5, inner=None, order=['constant', 'increase', 'decrease'])
  841. g.map(
  842. sns.pointplot,
  843. "dbs",
  844. "time_from_onset_to_first_low",
  845. alpha=0.8,
  846. markersize=3,
  847. capsize=0.1,
  848. lw=2,
  849. order=["constant", "increase", "decrease"],
  850. )
  851. g.add_legend()
  852. g.set_titles("{col_name}")
  853. g.set_axis_labels("DBS policy", "Time (s)")
  854. g.fig.suptitle("Time used to reach personalized threshold", y=1.05)
  855. plt.savefig(
  856. "figures/testing_session_time_to_first_low.png", dpi=300, bbox_inches="tight"
  857. )
  858. # %%
  859. # ANOVA test
  860. for subj in ["patient1", "patient2"]:
  861. df_plot_subj = df_plot.filter(
  862. pl.col("subj") == subj, pl.col("time_from_onset_to_first_low").is_not_null()
  863. )
  864. model = ols(
  865. formula="time_from_onset_to_first_low ~ dbs*condition",
  866. data=df_plot_subj.to_pandas(),
  867. ).fit()
  868. print(f"{subj}:\n", sm.stats.anova_lm(model, typ=2), "\n")
  869. # %%
  870. # posthoc t-test comparison between regulate and rest under each DBS policy, print out t-test result and Cohen's d
  871. for subj in ["patient1", "patient2"]:
  872. df_plot_subj = df_plot.filter(
  873. pl.col("subj") == subj, pl.col("time_from_onset_to_first_low").is_not_null()
  874. )
  875. df_pd = df_plot_subj.to_pandas()
  876. print(f"Posthoc t-test between regulate and rest for {subj}:")
  877. for dbs_policy in ["constant", "increase", "decrease"]:
  878. group_reg = (
  879. df_pd[(df_pd["dbs"] == dbs_policy) & (df_pd["condition"] == "regulate")][
  880. "time_from_onset_to_first_low"
  881. ]
  882. .dropna()
  883. .values
  884. )
  885. group_rest = (
  886. df_pd[(df_pd["dbs"] == dbs_policy) & (df_pd["condition"] == "rest")][
  887. "time_from_onset_to_first_low"
  888. ]
  889. .dropna()
  890. .values
  891. )
  892. if len(group_reg) > 1 and len(group_rest) > 1:
  893. t_stat, p_val = ttest_ind(group_reg, group_rest, equal_var=False)
  894. d = utils.cohens_d(group_reg, group_rest)
  895. print(f" DBS: {dbs_policy}")
  896. print(
  897. f" t({len(group_reg) + len(group_rest) - 2}): {t_stat:.3f}, p = {p_val:.3g}, Cohen's d = {d:.3f}"
  898. )
  899. else:
  900. print(f" DBS: {dbs_policy}")
  901. print(" Not enough data for t-test")
  902. print("")
  903. # %% [markdown]
  904. # ## Motor performance data
  905. # %%
  906. def read_key_tapping_summary_data(subj, dbs_conditions=["constant", "increase", "decrease"]):
  907. file = f"data/motor_data/key_tapping_{subj}.csv"
  908. df = pl.read_csv(file)
  909. df_filtered = df.filter(pl.col("DBS Condition").is_in(dbs_conditions))
  910. df_filtered = df_filtered.rename({"DBS Condition": "DBS_Condition"})
  911. # Add Pre vs. Post Label
  912. df_filtered = df_filtered.with_columns(
  913. subj=pl.lit(subj), date=pl.col("Session Date").rank(method="dense")
  914. )
  915. if subj == "RCS05": # RCS05 has only one constant pre key tapping and 3 post key tapping (constant, increase, decrease)
  916. df_filtered = df_filtered.with_columns(
  917. task_order_num=pl.col("File Name").rank(method="dense").over(["date"])
  918. ).with_columns(
  919. task_order_num_max=pl.col("task_order_num").max().over(["date"]),
  920. task_order=pl.when(pl.col("task_order_num") == 1)
  921. .then(pl.lit("pre-BCI"))
  922. .otherwise(pl.lit("post-BCI"))
  923. )
  924. elif subj == "RCS15":
  925. # RCS15 has constant, increase, and decrease pre and post key tapping
  926. # 2024-10-29 file: (1 pre, 1 post)*3 = 6 files; later files: (1 pre, 1 after block 1, 1 after block 2)*3 = 9 files
  927. df_filtered = df_filtered.with_columns(
  928. task_order_num=pl.col("File Name")
  929. .rank(method="dense")
  930. .over(["date", "DBS_Condition"])
  931. ).with_columns(
  932. task_order_num_max=pl.col("task_order_num").max().over(["date", "DBS_Condition"])
  933. ).with_columns(
  934. task_order=pl.when(pl.col("task_order_num") == 1)
  935. .then(pl.lit("pre-BCI"))
  936. .when(pl.col("task_order_num") == pl.col("task_order_num_max"))
  937. .then(pl.lit("post-BCI"))
  938. .otherwise(pl.lit("middle-BCI"))
  939. )
  940. return df_filtered
  941. # %%
  942. df_key_tapping_rcs05 = read_key_tapping_summary_data("RCS05")
  943. df_key_tapping_rcs15 = read_key_tapping_summary_data("RCS15")
  944. df_key_tapping_all = pl.concat(
  945. (df_key_tapping_rcs05, df_key_tapping_rcs15)
  946. ).with_columns(
  947. subj=pl.col("subj").replace({"RCS05": "patient1", "RCS15": "patient2"}),
  948. trial_id=(pl.col("date") - 1) * 10 + pl.col("Trial"),
  949. tapping_speed=1/pl.col("avg_rt")
  950. )
  951. df_key_tapping_all
  952. # %%
  953. outcome_var = "avg_rt"
  954. # outcome_var = "Errors"
  955. # outcome_var = "tapping_speed"
  956. fig, ax = plt.subplots(1, 2, figsize=(8, 3))
  957. for i, subj in enumerate(["patient1", "patient2"]):
  958. df_plot = df_key_tapping_all.filter(
  959. pl.col("subj") == subj,
  960. pl.col("DBS_Condition") == "constant",
  961. pl.col("task_order").is_in(["pre-BCI", "post-BCI"]),
  962. )
  963. legend = "full" if subj == "patient2" else False
  964. # Violin plot (distribution of reaction times)
  965. sns.violinplot(
  966. data=df_plot.to_pandas(),
  967. x="task_order",
  968. y=outcome_var,
  969. hue="task_order",
  970. inner="point",
  971. inner_kws={"alpha": 0.05},
  972. palette="Paired",
  973. legend=legend,
  974. linewidth=1.2,
  975. alpha=0.8,
  976. ax=ax[i],
  977. )
  978. sns.lineplot(
  979. data=df_plot.to_pandas(),
  980. x="task_order",
  981. y=outcome_var,
  982. color="gray",
  983. units="trial_id",
  984. alpha=0.1,
  985. estimator=None,
  986. ax=ax[i],
  987. )
  988. # Overlay point plot (mean reaction times with standard error)
  989. sns.pointplot(
  990. data=df_plot.to_pandas(),
  991. x="task_order",
  992. y=outcome_var,
  993. estimator="mean",
  994. color="black",
  995. alpha=0.5,
  996. markers="o",
  997. markersize=5,
  998. lw=2,
  999. capsize=0.1,
  1000. ax=ax[i],
  1001. )
  1002. ax[i].set_title(subj, fontsize=12, fontweight="bold")
  1003. ax[i].set_xlabel("", fontsize=12)
  1004. # Titles and labels
  1005. if outcome_var == "avg_rt":
  1006. # ax[i].set_ylim(None, 0.4)
  1007. if subj == "patient1":
  1008. ax[i].set_ylabel("inter-tap interval (s)", fontsize=12)
  1009. else:
  1010. ax[i].set_ylabel(None, fontsize=12)
  1011. elif outcome_var == "Errors":
  1012. ax[i].set_ylim(None, 3)
  1013. if subj == "patient1":
  1014. ax[i].set_ylabel("number of errors", fontsize=12)
  1015. else:
  1016. ax[i].set_ylabel(None, fontsize=12)
  1017. sns.move_legend(ax[1], "center left", bbox_to_anchor=(1, 0.5), title="Timing")
  1018. plt.suptitle("Key tapping performance", y=1.03, fontsize=14, fontweight="bold")
  1019. # plt.savefig(f"figures/key_tapping_{outcome_var}_constantDBS.png", dpi=300, bbox_inches="tight")
  1020. # %%
  1021. # Run t-test comparing outcome_var between task_order for Patient 1 and Patient 2 separately
  1022. outcome_var = "avg_rt"
  1023. for subj in ["patient1", "patient2"]:
  1024. df_subj = df_key_tapping_all.filter(
  1025. pl.col("subj") == subj,
  1026. pl.col("DBS_Condition") == "constant",
  1027. ).sort("Session Number", "Trial")
  1028. group1_vals = df_subj.filter(pl.col("task_order") == "pre-BCI")[outcome_var].to_numpy()
  1029. group2_vals = df_subj.filter(pl.col("task_order") == "post-BCI")[outcome_var].to_numpy()
  1030. # Paired t-test using group1_vals and group2_vals (assuming order is matched)
  1031. if len(group1_vals) == len(group2_vals) and len(group1_vals) > 0:
  1032. tstat_paired, pval_paired = ttest_rel(group1_vals, group2_vals, nan_policy='omit')
  1033. n_paired = len(group1_vals)
  1034. df_paired = n_paired - 1
  1035. mean1_paired, mean2_paired = group1_vals.mean(), group2_vals.mean()
  1036. diff = group1_vals - group2_vals
  1037. pooled_sd_paired = diff.std(ddof=1)
  1038. cohend_paired = (mean1_paired - mean2_paired) / pooled_sd_paired if pooled_sd_paired != 0 else np.nan
  1039. print(f"{subj}: Paired t-test between pre-BCI and post-BCI for '{outcome_var}': t={tstat_paired:.3f}, p={pval_paired:.3g}, df={df_paired}, Cohen's d={cohend_paired:.3f}")
  1040. elif len(group1_vals) != len(group2_vals):
  1041. print(f"{subj}: Cannot run paired t-test; groups have unequal length ({len(group1_vals)} vs {len(group2_vals)}).")

analysis.ipynb at commit b2b11d5, no license · at the source

Overview

Authors: Jin-Xiao Zhang1, Jiyeon Suh1, Pria Daniel2, Philip Starr3, Jeffrey Herron4, Simon Little1
  1. Department of Neurology, University of California, San Francisco
  2. Department of Psychology, University of California, San Diego
  3. Department of Neurological Surgery, University of California, San Francisco
  4. Department of Neurological Surgery, University of Washington
Institutions: University of California, San Francisco (United States); University of Washington (United States)
Dates: published online 14 August 2026
Type: Preprint
License: CC BY-NC-ND
Identifiers: DOI 10.64898/2026.08.12.26350419 · OpenAlex W7203457671
Open access: green, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), Parkinson's (population), clinical / translational (subfield)
Methods: Spectral & time-frequency, Statistics
Topic: Neurological disorders and treatments (Neurology, Medicine), according to OpenAlex
Funding: Wellcome Trust (226645/Z/22/Z)
Citations: not cited yet (Europe PMC); 38 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

Its files are read in the Code ↔ Paper reader above, with 2 matches between paragraphs and lines of code.

tepzhang/volitional_DBS

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: b2b11d54e1dab1d173ace3dd004af95761ba7db2, 20 March 2026
Languages: Python (2), Jupyter (1)
Size: 93 files, 3 scripts
Software Heritage: not archived
Found in: “Data and materials availability”
Holds: 1 notebook
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (2 files), SciPy (2 files), Matplotlib (1 file), seaborn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
3 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 3 scripts, each with its path and the digest of its content;
  • 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.64898/2026.08.12.26350419.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, journal, dates, 6 authors, 1 funder, 35 references.

Cite

This paper

Zhang, J.-X., Suh, J., Daniel, P., Starr, P., Herron, J., & Little, S. (2026). Volitional deep brain stimulation following brain-computer interface training for Parkinson’s disease. medRxiv (preprint). https://doi.org/10.64898/2026.08.12.26350419

BibTeX

@article{zhang2026volitional,
author = {Zhang, Jin-Xiao and Suh, Jiyeon and Daniel, Pria and Starr, Philip and Herron, Jeffrey and Little, Simon},
title = {{Volitional deep brain stimulation following brain-computer interface training for Parkinson’s disease}},
journal = {medRxiv (preprint)},
year = {2026},
month = aug,
publisher = {medRxiv},
doi = {10.64898/2026.08.12.26350419},
url = {https://doi.org/10.64898/2026.08.12.26350419}
}

RIS

TY - JOUR
AU - Zhang, Jin-Xiao
AU - Suh, Jiyeon
AU - Daniel, Pria
AU - Starr, Philip
AU - Herron, Jeffrey
AU - Little, Simon
TI - Volitional deep brain stimulation following brain-computer interface training for Parkinson’s disease
T2 - medRxiv (preprint)
J2 - medRxiv
PY - 2026
DA - 2026/08/14
PB - medRxiv
DO - 10.64898/2026.08.12.26350419
UR - https://doi.org/10.64898/2026.08.12.26350419
ER -

CSL-JSON

{
"id": "10.64898/2026.08.12.26350419",
"type": "article",
"title": "Volitional deep brain stimulation following brain-computer interface training for Parkinson’s disease",
"container-title": "medRxiv (preprint)",
"author": [
{
"family": "Zhang",
"given": "Jin-Xiao"
},
{
"family": "Suh",
"given": "Jiyeon"
},
{
"family": "Daniel",
"given": "Pria"
},
{
"family": "Starr",
"given": "Philip"
},
{
"family": "Herron",
"given": "Jeffrey"
},
{
"family": "Little",
"given": "Simon"
}
],
"container-title-short": "medRxiv",
"DOI": "10.64898/2026.08.12.26350419",
"publisher": "medRxiv",
"URL": "https://doi.org/10.64898/2026.08.12.26350419",
"issued": {
"date-parts": [
[
2026,
8,
14
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1016/j.ebiom.2026.106293 [code]
Dynamic neural states underpin motor symptom severity in Parkinson's disease: a longitudinal analysis of chronic cortico-subthalamic nucleus recordings.
Journal: EBioMedicine
In common: statsmodels, seaborn, SciPy, 2 other tools, Parkinson's, 5 references
[2] doi:10.1038/s41591-026-04432-4 [code]
Activity-dependent adaptive deep brain stimulation improves gait in Parkinson's disease.
Journal: Nature medicine
In common: SciPy, Matplotlib, NumPy, Parkinson's, clinical / translational, 5 references
[3] doi:10.1038/s41591-026-04434-2 [code]
Adaptive deep brain stimulation for dynamic gait control in Parkinson's disease: a randomized feasibility trial.
Journal: Nature medicine
In common: SciPy, Matplotlib, NumPy, Parkinson's, clinical / translational, 4 references
[4] doi:10.1038/s41467-026-70633-7 [code]
Global coincident bursts of high frequency oscillations across the human cortex coordinate large-scale memory processing.
Journal: Nature communications
In common: statsmodels, seaborn, SciPy, 2 other tools, 2 references
[5] doi:10.1038/s41593-026-02228-w [code]
Circuit response to neuromodulation characterized with simultaneous deep brain stimulation and precision neuroimaging in humans.
Journal: Nature neuroscience
In common: statsmodels, seaborn, SciPy, 2 other tools, Parkinson's, 1 reference
[6] doi:10.1038/s41531-026-01531-4 [code]
Inconsistent subthalamic local field potential beta activity amid in- and antiphasic neuronal bursts.
Journal: NPJ Parkinson's disease
In common: SciPy, Matplotlib, NumPy, Parkinson's, clinical / translational, 2 references
[7] doi:10.1017/s0033291726104103 [code]
Linking brain structure to stress reactivity: cingulate surface area predicts acute cortisol responses.
Journal: Psychological medicine
In common: statsmodels, seaborn, SciPy, 2 other tools, 1 reference
[8] doi:10.1162/imag.a.1226 [code]
A neuroscientist's guide to neural burst detection.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: statsmodels, seaborn, SciPy, 2 other tools, 1 reference
[9] doi:10.1038/s43856-026-01606-6 [code]
Validation of remote multimodal AI screening for Parkinson disease across diverse settings.
Journal: Communications medicine
In common: statsmodels, seaborn, SciPy, 2 other tools, Parkinson's, clinical / translational
[10] doi:10.1038/s41531-026-01380-1 [code]
Identifying maximal beta power from directional subthalamic local field potentials in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: statsmodels, seaborn, SciPy, 2 other tools, Parkinson's, clinical / translational

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.