pylance standardization
This commit is contained in:
@@ -8,12 +8,13 @@ License: GPL-3.0
|
||||
|
||||
# Built-in imports
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import platform
|
||||
import threading
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, Optional, cast, Literal, Union
|
||||
from typing import Any, Optional, Sequence, cast, Literal, Union
|
||||
from itertools import compress
|
||||
from copy import deepcopy
|
||||
from multiprocessing import Queue, Pool
|
||||
@@ -1765,29 +1766,6 @@ def _check_load_fold(fold_files, atlas):
|
||||
|
||||
|
||||
|
||||
def fold_channel_specificity_normal(raw, fold_files=None, atlas="Juelich", interpolate=False):
|
||||
"""Return the landmarks and specificity a channel is sensitive to.
|
||||
|
||||
Parameters
|
||||
|
||||
""" # noqa: E501
|
||||
_validate_type(raw, BaseRaw, "raw")
|
||||
|
||||
reference_locations = generate_montage_locations()
|
||||
|
||||
fold_tbl = _check_load_fold(fold_files, atlas)
|
||||
|
||||
chan_spec = list()
|
||||
for cidx in range(len(raw.ch_names)):
|
||||
tbl = _source_detector_fold_table(
|
||||
raw, cidx, reference_locations, fold_tbl, interpolate
|
||||
)
|
||||
chan_spec.append(tbl.reset_index(drop=True))
|
||||
|
||||
return chan_spec
|
||||
|
||||
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""
|
||||
Get absolute path to resource regardless of running directly or packaged using PyInstaller
|
||||
@@ -1958,35 +1936,42 @@ def resource_path(relative_path):
|
||||
|
||||
|
||||
|
||||
def fold_channels(raw: BaseRaw, p_name: str, progress_queue=None) -> dict[str, list[dict[str, Any]]]:
|
||||
def fold_channels(raw: BaseRaw, p_name: str, atlas: str='Brodmann', progress_queue=None) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Runs in background process.
|
||||
Does only heavy math/lookup. Returns data instead of a static image.
|
||||
"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
set_config('MNE_NIRS_FOLD_PATH', resource_path("./mne_data/fOLD/fOLD-public-master/Supplementary"))
|
||||
fold_dir = resource_path("./mne_data/fOLD/fOLD-public-master/Supplementary")
|
||||
else:
|
||||
path = os.path.expanduser("~") + "/mne_data/fOLD/fOLD-public-master/Supplementary"
|
||||
set_config('MNE_NIRS_FOLD_PATH', resource_path(path))
|
||||
fold_dir = resource_path(path)
|
||||
|
||||
set_config('MNE_NIRS_FOLD_PATH', fold_dir)
|
||||
|
||||
hbo_channel_names = cast(list[str], getattr(raw.copy().pick(picks='hbo'), "ch_names"))
|
||||
|
||||
# Store clean, picklable data lists instead of complex DataFrames
|
||||
channel_results = {}
|
||||
_validate_type(raw, BaseRaw, "raw")
|
||||
|
||||
reference_locations = generate_montage_locations()
|
||||
|
||||
fold_tbl = _check_load_fold(fold_files=fold_dir, atlas=atlas)
|
||||
|
||||
channel_results = {}
|
||||
step_idx = 0
|
||||
|
||||
for channel_name in hbo_channel_names:
|
||||
channel_data = raw.copy().pick(picks=channel_name)
|
||||
output = cast(list[DataFrame], fold_channel_specificity_normal(channel_data, interpolate=True, atlas='Brodmann'))
|
||||
for cidx, channel_name in enumerate(hbo_channel_names):
|
||||
tbl = _source_detector_fold_table(
|
||||
raw, cidx, reference_locations, fold_tbl, interpolate=True
|
||||
)
|
||||
|
||||
channel_results[channel_name] = []
|
||||
for df_data in output:
|
||||
# Extract just raw primitive types so they transfer over process channels flawlessly
|
||||
for _, row in df_data.iterrows():
|
||||
channel_results[channel_name].append({
|
||||
'Landmark': str(row['Landmark']),
|
||||
'Specificity': float(row['Specificity'])
|
||||
})
|
||||
|
||||
for _, row in tbl.iterrows():
|
||||
channel_results[channel_name].append({
|
||||
'Landmark': str(row['Landmark']),
|
||||
'Specificity': float(row['Specificity'])
|
||||
})
|
||||
|
||||
step_idx += 1
|
||||
if progress_queue is not None:
|
||||
progress_queue.put((p_name, step_idx))
|
||||
@@ -2180,7 +2165,7 @@ def plot_3d_evoked_array(
|
||||
return brain
|
||||
|
||||
|
||||
def aggregate_fnirs_group_geometry(raw_list):
|
||||
def aggregate_fnirs_group_geometry(raw_list: Sequence[BaseRaw | None]) -> BaseRaw:
|
||||
"""
|
||||
Averages fNIRS geometry across participants in two tiers:
|
||||
1. Average by Channel Pairing (S_D).
|
||||
@@ -2246,7 +2231,15 @@ def aggregate_fnirs_group_geometry(raw_list):
|
||||
|
||||
|
||||
|
||||
def brain_3d_visualization(raw_haemo, df_cha, selected_event, t_or_theta: Literal['t', 'theta'] = 'theta', show_optodes: Literal['sensors', 'labels', 'none', 'all'] = 'all', show_text: bool = True, brain_bounds: float = 1.0) -> None:
|
||||
def brain_3d_visualization(
|
||||
raw_haemo: BaseRaw | None,
|
||||
df_cha: DataFrame | None,
|
||||
selected_event: str | None,
|
||||
t_or_theta: Literal["t", "theta"] = "theta",
|
||||
show_optodes: Literal["sensors", "labels", "none", "all"] = "all",
|
||||
show_text: bool = True,
|
||||
brain_bounds: float | tuple[float, float] | Sequence[float] = 1.0,
|
||||
) -> None:
|
||||
|
||||
clim = dict(kind="value", pos_lims=(0, brain_bounds/2, brain_bounds))
|
||||
|
||||
@@ -2582,7 +2575,14 @@ def plot_2d_3d_contrasts_between_groups(
|
||||
|
||||
|
||||
|
||||
def plot_fir_model_results(df, raw_haemo, dm, selected_event, l_bound, u_bound):
|
||||
def plot_fir_model_results(
|
||||
df: DataFrame,
|
||||
raw_haemo: BaseRaw | None,
|
||||
dm: DataFrame | None,
|
||||
selected_event: str | None,
|
||||
l_bound: float,
|
||||
u_bound: float,
|
||||
) -> None:
|
||||
|
||||
|
||||
df["isActivity"] = [f"{selected_event}" in n for n in df["Condition"]]
|
||||
@@ -2801,11 +2801,19 @@ def load_snirf(file_path: str) -> tuple[BaseRaw, Figure]:
|
||||
|
||||
|
||||
|
||||
def run_roi_second_level_analysis(df_roi_all, df_cha_all=None, raw_haemo=None,
|
||||
p_threshold=0.05, min_subjects=5,
|
||||
correction_method='fdr_bh', target_chroma='hbo',
|
||||
graph_bounds=None, roi_config=None,
|
||||
threshold_topo=False): # Added parameter
|
||||
def run_roi_second_level_analysis(
|
||||
df_roi_all: DataFrame,
|
||||
df_cha_all: DataFrame | None = None,
|
||||
raw_haemo: BaseRaw | None = None,
|
||||
p_threshold: float = 0.05,
|
||||
min_subjects: int = 5,
|
||||
correction_method: str | None = "fdr_bh",
|
||||
target_chroma: str = "hbo",
|
||||
graph_bounds: float | None = None,
|
||||
roi_config: str | Path | None = None,
|
||||
threshold_topo: bool = False,
|
||||
) -> DataFrame:
|
||||
|
||||
"""
|
||||
Perform group-level ROI analysis, prints stats to console, plots the ROI bar chart,
|
||||
and dynamically plots isolated channel-level group topography maps based on a JSON config.
|
||||
@@ -3064,13 +3072,24 @@ def clean_subject_id(path_or_id):
|
||||
|
||||
|
||||
|
||||
def run_cross_group_second_level_analysis(df_roi_all, file_paths_a, file_paths_b,
|
||||
group_a_name="Group A", group_b_name="Group B",
|
||||
df_cha_all=None, raw_haemo=None,
|
||||
p_threshold=0.05, min_subjects=3,
|
||||
correction_method='fdr_bh', target_chroma='hbo',
|
||||
selected_event=None, graph_bounds=None,
|
||||
roi_config=None, threshold_topo=False):
|
||||
def run_cross_group_second_level_analysis(
|
||||
df_roi_all: DataFrame,
|
||||
file_paths_a: list[str],
|
||||
file_paths_b: list[str],
|
||||
group_a_name: str = "Group A",
|
||||
group_b_name: str = "Group B",
|
||||
df_cha_all: DataFrame | None = None,
|
||||
raw_haemo: Any = None,
|
||||
p_threshold: float = 0.05,
|
||||
min_subjects: int = 3,
|
||||
correction_method: str | None = "fdr_bh",
|
||||
target_chroma: str = "hbo",
|
||||
selected_event: str | None = None,
|
||||
graph_bounds: tuple[float, float] | list[float] | None = None,
|
||||
roi_config: Path | str | None = None,
|
||||
threshold_topo: bool = False,
|
||||
) -> DataFrame:
|
||||
|
||||
"""
|
||||
Perform cross-group independent statistical analyses (Group A vs Group B),
|
||||
renders a grouped bar chart with significance brackets, and plots a group-contrast topography map.
|
||||
@@ -3331,11 +3350,21 @@ def run_cross_group_second_level_analysis(df_roi_all, file_paths_a, file_paths_b
|
||||
|
||||
|
||||
|
||||
def run_cross_group_laterality_analysis(df_roi_all_a, df_roi_all_b, roi_pairs, condition,
|
||||
group_a_name="Group A", group_b_name="Group B",
|
||||
target_chroma='hbo', min_subjects=3,
|
||||
p_threshold=0.05, correction_method=None,
|
||||
roi_contra_label=None, roi_ipsi_label=None):
|
||||
def run_cross_group_laterality_analysis(
|
||||
df_roi_all_a: DataFrame,
|
||||
df_roi_all_b: DataFrame,
|
||||
roi_pairs: tuple[str, str] | None,
|
||||
condition: str | None,
|
||||
group_a_name: str = "Group A",
|
||||
group_b_name: str = "Group B",
|
||||
target_chroma: str = "hbo",
|
||||
min_subjects: int = 3,
|
||||
p_threshold: float = 0.05,
|
||||
correction_method: str | None = None,
|
||||
roi_contra_label: str | None = None,
|
||||
roi_ipsi_label: str | None = None,
|
||||
) -> DataFrame:
|
||||
|
||||
"""
|
||||
Compare LATERALITY between two independent groups of subjects (e.g. a
|
||||
control group vs. a target group), using Welch's t-test on each
|
||||
@@ -3582,11 +3611,20 @@ def run_cross_group_laterality_analysis(df_roi_all_a, df_roi_all_b, roi_pairs, c
|
||||
|
||||
|
||||
|
||||
def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_name, roi_json_path,
|
||||
group_a_name="Group A", group_b_name="Group B",
|
||||
target_chroma='hbo', min_subjects=3,
|
||||
p_threshold=0.05, correction_method='fdr_bh',
|
||||
weighted=True):
|
||||
def run_cross_group_contrast_analysis(
|
||||
df_contrasts_a: DataFrame,
|
||||
df_contrasts_b: DataFrame,
|
||||
contrast_name: str,
|
||||
roi_json_path: str | Path | None,
|
||||
group_a_name: str = "Group A",
|
||||
group_b_name: str = "Group B",
|
||||
target_chroma: str = "hbo",
|
||||
min_subjects: int = 3,
|
||||
p_threshold: float = 0.05,
|
||||
correction_method: str = "fdr_bh",
|
||||
weighted: bool = True,
|
||||
) -> DataFrame:
|
||||
|
||||
"""
|
||||
Compare a JOINT-FIT TASK CONTRAST (e.g. '2.0_vs_3.0'), aggregated to ROI
|
||||
level, between two independent groups. This is the cross-group analog
|
||||
@@ -3665,10 +3703,10 @@ def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_n
|
||||
|
||||
if df_a_filt.empty:
|
||||
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_a_name}'s data.")
|
||||
return pd.DataFrame()
|
||||
return DataFrame()
|
||||
if df_b_filt.empty:
|
||||
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_b_name}'s data.")
|
||||
return pd.DataFrame()
|
||||
return DataFrame()
|
||||
|
||||
roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_json_path, weighted=weighted)
|
||||
roi_b = aggregate_channel_contrasts_to_roi(df_b_filt, roi_json_path, weighted=weighted)
|
||||
@@ -3679,7 +3717,7 @@ def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_n
|
||||
if roi_a.empty or roi_b.empty:
|
||||
print(f"[ERROR] No ROI-aggregated values produced for one or both groups "
|
||||
f"(check regions.json channel names against this montage).")
|
||||
return pd.DataFrame()
|
||||
return DataFrame()
|
||||
|
||||
all_rois = sorted(set(roi_a['ROI'].unique()) | set(roi_b['ROI'].unique()))
|
||||
results = []
|
||||
@@ -3798,10 +3836,17 @@ def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_n
|
||||
|
||||
|
||||
|
||||
def run_roi_paired_contrast_analysis(df_roi_all, roi_pairs, condition,
|
||||
target_chroma='hbo', min_subjects=5,
|
||||
p_threshold=0.05, correction_method=None,
|
||||
roi_a_label=None, roi_b_label=None):
|
||||
def run_roi_paired_contrast_analysis(
|
||||
df_roi_all: DataFrame,
|
||||
roi_pairs: Sequence[tuple[str, str]] | list[list[str]],
|
||||
condition: str,
|
||||
target_chroma: str = 'hbo',
|
||||
min_subjects: int = 5,
|
||||
p_threshold: float = 0.05,
|
||||
correction_method: str | None = None,
|
||||
roi_a_label: str | None = None,
|
||||
roi_b_label: str | None = None,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Paired within-subject ROI contrast (e.g. contralateral minus ipsilateral
|
||||
motor ROI), as a companion to run_roi_second_level_analysis rather than a
|
||||
@@ -4012,7 +4057,11 @@ def run_roi_paired_contrast_analysis(df_roi_all, roi_pairs, condition,
|
||||
|
||||
|
||||
|
||||
def aggregate_channel_contrasts_to_roi(df_contrasts, roi_json_path, weighted=True):
|
||||
def aggregate_channel_contrasts_to_roi(
|
||||
df_contrasts: DataFrame,
|
||||
roi_json_path: str | Path | None,
|
||||
weighted: bool = True
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Combine already-computed per-channel CONTRAST results (e.g. your
|
||||
'2.0_vs_3.0' rows from contrasts.csv / contrast_results) into
|
||||
@@ -5519,13 +5568,13 @@ def process_participant(file_path, progress_callback=None):
|
||||
if BAD_CHANNELS_HANDLING != "None" and not FOLDING_BYP:
|
||||
raw, fig_dropped, fig_raw_before, bad_channels = mark_bads(raw, bad_sci, bad_snr, bad_psp, bad_coeff_var, bad_amplitude_range, bad_noise, bad_disp)
|
||||
if fig_dropped and fig_raw_before is not None:
|
||||
fig_individual["fig2"] = fig_dropped
|
||||
fig_individual["fig3"] = fig_raw_before
|
||||
fig_individual["Bad Channels by Method"] = fig_dropped
|
||||
fig_individual["Bad Channels Data"] = fig_raw_before
|
||||
if bad_channels:
|
||||
if BAD_CHANNELS_HANDLING == "Interpolate":
|
||||
raw, fig_raw_after, fig_compare = interpolate_fNIRS_bads_weighted_average(raw, max_dist=MAX_DIST, min_neighbors=MIN_NEIGHBORS)
|
||||
fig_individual["fig4"] = fig_raw_after
|
||||
fig_individual["Compare"] = fig_compare
|
||||
fig_individual["Data after Interpolating Bad Channels"] = fig_raw_after
|
||||
fig_individual["Bad Channels Interpolation Results"] = fig_compare
|
||||
elif BAD_CHANNELS_HANDLING == "Remove":
|
||||
raw = remove_bad_channels(raw, bad_channels)
|
||||
if progress_callback: progress_callback(13)
|
||||
@@ -5542,7 +5591,7 @@ def process_participant(file_path, progress_callback=None):
|
||||
if TDDR and not FOLDING_BYP:
|
||||
raw_od = temporal_derivative_distribution_repair(raw_od)
|
||||
fig_raw_od_tddr = raw_od.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="After TDDR (Motion Correction)", show=False)
|
||||
fig_individual["TDDR"] = fig_raw_od_tddr
|
||||
fig_individual["Temporal Derivative Distribution Repair"] = fig_raw_od_tddr
|
||||
if progress_callback: progress_callback(15)
|
||||
logger.info("Step 15 Completed.")
|
||||
|
||||
@@ -5556,7 +5605,7 @@ def process_participant(file_path, progress_callback=None):
|
||||
# Step 17: Haemoglobin Concentration
|
||||
raw_haemo = beer_lambert_law(raw_od, ppf=calculate_dpf(file_path))
|
||||
fig_raw_haemo_bll = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="HbO and HbR Signals", show=False)
|
||||
fig_individual["BLL"] = fig_raw_haemo_bll
|
||||
fig_individual["Modified Beer Lambert Law"] = fig_raw_haemo_bll
|
||||
if progress_callback: progress_callback(17)
|
||||
logger.info("Step 17 Completed.")
|
||||
|
||||
@@ -5564,15 +5613,15 @@ def process_participant(file_path, progress_callback=None):
|
||||
if ENHANCE_NEGATIVE_CORRELATION and not FOLDING_BYP:
|
||||
raw_haemo = enhance_negative_correlation(raw_haemo)
|
||||
fig_raw_haemo_enc = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Enhance Negative Correlation", show=False)
|
||||
fig_individual["ENC"] = fig_raw_haemo_enc
|
||||
fig_individual["Enhance Negative Correlation"] = fig_raw_haemo_enc
|
||||
if progress_callback: progress_callback(18)
|
||||
logger.info("Step 18 Completed.")
|
||||
|
||||
# Step 19: Filter
|
||||
if FILTER and not FOLDING_BYP:
|
||||
raw_haemo, fig_filter, fig_raw_haemo_filter = filter_the_data(raw_haemo)
|
||||
fig_individual["filter1"] = fig_filter
|
||||
fig_individual["filter2"] = fig_raw_haemo_filter
|
||||
fig_individual["Filter_1"] = fig_filter
|
||||
fig_individual["Filter_2"] = fig_raw_haemo_filter
|
||||
if progress_callback: progress_callback(19)
|
||||
logger.info("Step 19 Completed.")
|
||||
|
||||
@@ -5580,7 +5629,7 @@ def process_participant(file_path, progress_callback=None):
|
||||
if not FOLDING_BYP:
|
||||
events, event_dict = events_from_annotations(raw_haemo)
|
||||
fig_events = plot_events(events, event_id=event_dict, sfreq=raw_haemo.info["sfreq"], show=False)
|
||||
fig_individual["events"] = fig_events
|
||||
fig_individual["Events"] = fig_events
|
||||
if progress_callback: progress_callback(20)
|
||||
logger.info("Step 20 Completed.")
|
||||
|
||||
@@ -5652,7 +5701,11 @@ def sanitize_paths_for_pickle(raw_haemo, epochs):
|
||||
epochs._raw._filenames = [str(p) for p in epochs._raw._filenames]
|
||||
|
||||
|
||||
def functional_connectivity_spectral_epochs(epochs, n_lines, vmin):
|
||||
def functional_connectivity_spectral_epochs(
|
||||
epochs: DataFrame | None,
|
||||
n_lines: int,
|
||||
vmin: float,
|
||||
) -> None:
|
||||
|
||||
# will crash without this load
|
||||
epochs.load_data()
|
||||
@@ -5691,7 +5744,11 @@ def functional_connectivity_spectral_epochs(epochs, n_lines, vmin):
|
||||
|
||||
|
||||
|
||||
def functional_connectivity_spectral_time(epochs, n_lines, vmin):
|
||||
def functional_connectivity_spectral_time(
|
||||
epochs: DataFrame | None,
|
||||
n_lines: int,
|
||||
vmin: float,
|
||||
) -> None:
|
||||
|
||||
# will crash without this load
|
||||
epochs.load_data()
|
||||
@@ -5735,7 +5792,12 @@ def functional_connectivity_spectral_time(epochs, n_lines, vmin):
|
||||
|
||||
|
||||
|
||||
def functional_connectivity_envelope(epochs, n_lines, vmin):
|
||||
def functional_connectivity_envelope(
|
||||
epochs: DataFrame | None,
|
||||
n_lines: int,
|
||||
vmin: float,
|
||||
) -> None:
|
||||
|
||||
# will crash without this load
|
||||
|
||||
epochs.load_data()
|
||||
@@ -5765,7 +5827,12 @@ def functional_connectivity_envelope(epochs, n_lines, vmin):
|
||||
)
|
||||
|
||||
|
||||
def functional_connectivity_betas(raw_hbo, n_lines, vmin, event_name=None):
|
||||
def functional_connectivity_betas(
|
||||
raw_hbo: BaseRaw,
|
||||
n_lines: int,
|
||||
vmin: float,
|
||||
event_name: str | None = None,
|
||||
) -> None:
|
||||
|
||||
raw_hbo = raw_hbo.copy().pick(picks="hbo")
|
||||
onsets = raw_hbo.annotations.onset
|
||||
@@ -5966,7 +6033,15 @@ def get_single_subject_beta_corr(raw_hbo, event_name=None, config=None):
|
||||
return corr_matrix, raw_hbo.ch_names
|
||||
|
||||
|
||||
def run_group_functional_connectivity(haemo_dict, config_dict, selected_paths, event_name, n_lines, vmin):
|
||||
def run_group_functional_connectivity(
|
||||
haemo_dict: dict[str | Path, BaseRaw],
|
||||
config_dict: dict[str, Any],
|
||||
selected_paths: list[str],
|
||||
event_name: str | None,
|
||||
n_lines: int,
|
||||
vmin: float,
|
||||
) -> None:
|
||||
|
||||
"""Aggregates multiple participants and triggers the plot."""
|
||||
all_z_matrices = []
|
||||
common_names = None
|
||||
@@ -6067,4 +6142,28 @@ def run_group_functional_connectivity(haemo_dict, config_dict, selected_paths, e
|
||||
sig_avg_r, common_names, n_lines=n_lines,
|
||||
title=f"Group Connectivity: {event_name if event_name else 'All Events'}",
|
||||
vmin=vmin, vmax=1.0, colormap='hot'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def sparks_csv_export(
|
||||
haemo_obj: BaseRaw,
|
||||
save_path: str,
|
||||
) -> None:
|
||||
|
||||
raw = haemo_obj
|
||||
data, times = raw.get_data(return_times=True)
|
||||
ann_col = np.full(times.shape, "", dtype=object)
|
||||
|
||||
if raw.annotations is not None and len(raw.annotations) > 0:
|
||||
for onset, duration, desc in zip(
|
||||
raw.annotations.onset,
|
||||
raw.annotations.duration,
|
||||
raw.annotations.description
|
||||
):
|
||||
mask = (times >= onset) & (times < onset + duration)
|
||||
ann_col[mask] = desc
|
||||
|
||||
df = pd.DataFrame(data.T, columns=raw.ch_names)
|
||||
df.insert(0, "annotation", ann_col)
|
||||
df.insert(0, "time", times)
|
||||
df.to_csv(save_path, index=False)
|
||||
|
||||
Reference in New Issue
Block a user