spacr 0.3.33__py3-none-any.whl → 0.3.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
spacr/gui_utils.py CHANGED
@@ -508,7 +508,7 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
508
508
  imports = 1
509
509
  elif settings_type == 'ml_analyze':
510
510
  function = generate_ml_scores
511
- imports = 2
511
+ imports = 1
512
512
  elif settings_type == 'cellpose_masks':
513
513
  function = identify_masks_finetune
514
514
  imports = 1
spacr/ml.py CHANGED
@@ -730,7 +730,7 @@ def process_scores(df, dependent_variable, plate, min_cell_count=25, agg_type='m
730
730
 
731
731
  def generate_ml_scores(settings):
732
732
 
733
- from .io import _read_and_merge_data
733
+ from .io import _read_and_merge_data, _read_db
734
734
  from .plot import plot_plates
735
735
  from .utils import get_ml_results_paths
736
736
  from .settings import set_default_analyze_screen
@@ -753,6 +753,38 @@ def generate_ml_scores(settings):
753
753
  nuclei_limit,
754
754
  pathogen_limit,
755
755
  uninfected)
756
+
757
+ if settings['annotation_column'] is not None:
758
+
759
+ settings['location_column'] = settings['annotation_column']
760
+
761
+ png_list_df = _read_db(db_loc[0], tables=['png_list'])[0]
762
+ if not {'prcfo', settings['annotation_column']}.issubset(png_list_df.columns):
763
+ raise ValueError("The 'png_list_df' DataFrame must contain 'prcfo' and 'test' columns.")
764
+ annotated_df = png_list_df[['prcfo', settings['annotation_column']]].set_index('prcfo')
765
+ df = annotated_df.merge(df, left_index=True, right_index=True)
766
+ display(df)
767
+ unique_values = df[settings['annotation_column']].dropna().unique()
768
+ if len(unique_values) == 1:
769
+ unannotated_rows = df[df[settings['annotation_column']].isna()].index
770
+ existing_value = unique_values[0]
771
+ next_value = existing_value + 1
772
+
773
+ settings['positive_control'] = str(existing_value)
774
+ settings['negative_control'] = str(next_value)
775
+
776
+ existing_count = df[df[settings['annotation_column']] == existing_value].shape[0]
777
+ num_to_select = min(existing_count, len(unannotated_rows))
778
+ selected_rows = np.random.choice(unannotated_rows, size=num_to_select, replace=False)
779
+ df.loc[selected_rows, settings['annotation_column']] = next_value
780
+
781
+ # Print the counts for existing_value and next_value
782
+ existing_count_final = df[df[settings['annotation_column']] == existing_value].shape[0]
783
+ next_count_final = df[df[settings['annotation_column']] == next_value].shape[0]
784
+
785
+ print(f"Number of rows with value {existing_value}: {existing_count_final}")
786
+ print(f"Number of rows with value {next_value}: {next_count_final}")
787
+ df[settings['annotation_column']] = df[settings['annotation_column']].apply(str)
756
788
 
757
789
  if settings['channel_of_interest'] in [0,1,2,3]:
758
790
 
@@ -847,6 +879,7 @@ def ml_analysis(df, channel_of_interest=3, location_column='col', positive_contr
847
879
  if verbose:
848
880
  print(f'Found {len(features)} numerical features in the dataframe')
849
881
  print(f'Features used in training: {features}')
882
+
850
883
  df = pd.concat([df, df_metadata[location_column]], axis=1)
851
884
 
852
885
  # Subset the dataframe based on specified column values
spacr/plot.py CHANGED
@@ -23,6 +23,9 @@ import pingouin as pg
23
23
  from ipywidgets import IntSlider, interact
24
24
  from IPython.display import Image as ipyimage
25
25
 
26
+ import matplotlib.patches as patches
27
+ from collections import defaultdict
28
+
26
29
  def plot_image_mask_overlay(file, channels, cell_channel, nucleus_channel, pathogen_channel, figuresize=10, normalize=True, thickness=3, save_pdf=True):
27
30
  """Plot image and mask overlays."""
28
31
 
@@ -1930,7 +1933,7 @@ def jitterplot_by_annotation(src, x_column, y_column, plot_title='Jitter Plot',
1930
1933
 
1931
1934
  return balanced_df
1932
1935
 
1933
- def create_grouped_plot(df, grouping_column, data_column, graph_type='bar', summary_func='mean', order=None, colors=None, output_dir='./output', save=False, y_axis_start=None, error_bar_type='std'):
1936
+ def create_grouped_plot(df, grouping_column, data_column, graph_type='bar', summary_func='mean', order=None, colors=None, output_dir='./output', save=False, y_lim=None, error_bar_type='std'):
1934
1937
  """
1935
1938
  Create a grouped plot, perform statistical tests, and optionally export the results along with the plot.
1936
1939
 
@@ -1944,7 +1947,7 @@ def create_grouped_plot(df, grouping_column, data_column, graph_type='bar', summ
1944
1947
  - colors: List of colors for each group.
1945
1948
  - output_dir: Directory where the figure and test results will be saved if `save=True`.
1946
1949
  - save: Boolean flag indicating whether to save the plot and results to files.
1947
- - y_axis_start: Optional starting value for the y-axis.
1950
+ - y_lim: Optional y-axis min and max.
1948
1951
  - error_bar_type: Type of error bars to plot, either 'std' for standard deviation or 'sem' for standard error of the mean.
1949
1952
 
1950
1953
  Outputs:
@@ -2068,10 +2071,8 @@ def create_grouped_plot(df, grouping_column, data_column, graph_type='bar', summ
2068
2071
  results_df = pd.DataFrame(test_results)
2069
2072
 
2070
2073
  # Set y-axis start if provided
2071
- if y_axis_start is not None:
2072
- plt.ylim(bottom=y_axis_start)
2073
- else:
2074
- plt.ylim(0, None) # Default to starting at 0 if no custom start value is provided
2074
+ if isinstance(y_lim, list) and len(y_lim) == 2:
2075
+ plt.ylim(y_lim)
2075
2076
 
2076
2077
  # If save is True, save the plot and results as PNG and CSV
2077
2078
  if save:
@@ -2095,12 +2096,14 @@ def create_grouped_plot(df, grouping_column, data_column, graph_type='bar', summ
2095
2096
 
2096
2097
  class spacrGraph:
2097
2098
  def __init__(self, df, grouping_column, data_column, graph_type='bar', summary_func='mean',
2098
- order=None, colors=None, output_dir='./output', save=False, y_axis_start=None,
2099
+ order=None, colors=None, output_dir='./output', save=False, y_lim=None,
2099
2100
  error_bar_type='std', remove_outliers=False, theme='pastel', representation='object',
2100
2101
  paired=False, all_to_all=True, compare_group=None):
2102
+
2101
2103
  """
2102
2104
  Class for creating grouped plots with optional statistical tests and data preprocessing.
2103
2105
  """
2106
+
2104
2107
  self.df = df
2105
2108
  self.grouping_column = grouping_column
2106
2109
  self.data_column = data_column if isinstance(data_column, list) else [data_column]
@@ -2110,7 +2113,6 @@ class spacrGraph:
2110
2113
  self.colors = colors
2111
2114
  self.output_dir = output_dir
2112
2115
  self.save = save
2113
- self.y_axis_start = y_axis_start
2114
2116
  self.error_bar_type = error_bar_type
2115
2117
  self.remove_outliers = remove_outliers
2116
2118
  self.theme = theme
@@ -2118,14 +2120,15 @@ class spacrGraph:
2118
2120
  self.paired = paired
2119
2121
  self.all_to_all = all_to_all
2120
2122
  self.compare_group = compare_group
2121
-
2123
+ self.y_lim = y_lim
2122
2124
  self.results_df = pd.DataFrame()
2123
2125
  self.sns_palette = None
2124
- self.fig = None # To store the generated figure
2126
+ self.fig = None
2127
+
2128
+ self.results_name = str(self.data_column[0])+'_'+str(self.grouping_column)+'_'+str(self.graph_type)
2125
2129
 
2126
- # Preprocess and set palette
2127
2130
  self._set_theme()
2128
- self.raw_df = self.df.copy() # Preserve the raw data for n_object count
2131
+ self.raw_df = self.df.copy()
2129
2132
  self.df = self.preprocess_data()
2130
2133
 
2131
2134
  def _set_theme(self):
@@ -2150,19 +2153,15 @@ class spacrGraph:
2150
2153
  """Preprocess the data: remove NaNs, sort/order the grouping column, and optionally group by 'prc'."""
2151
2154
  # Remove NaNs in both the grouping column and each data column
2152
2155
  df = self.df.dropna(subset=[self.grouping_column] + self.data_column) # Handle multiple data columns
2153
-
2154
2156
  # Group by 'prc' column if representation is 'well'
2155
2157
  if self.representation == 'well':
2156
2158
  df = df.groupby(['prc', self.grouping_column])[self.data_column].agg(self.summary_func).reset_index()
2157
-
2158
2159
  if self.order:
2159
2160
  df[self.grouping_column] = pd.Categorical(df[self.grouping_column], categories=self.order, ordered=True)
2160
2161
  else:
2161
2162
  df[self.grouping_column] = pd.Categorical(df[self.grouping_column], categories=sorted(df[self.grouping_column].unique()), ordered=True)
2162
-
2163
2163
  return df
2164
2164
 
2165
-
2166
2165
  def remove_outliers_from_plot(self):
2167
2166
  """Remove outliers from the plot but keep them in the data."""
2168
2167
  filtered_df = self.df.copy()
@@ -2181,13 +2180,11 @@ class spacrGraph:
2181
2180
  """Perform normality tests for each group and each data column."""
2182
2181
  unique_groups = self.df[self.grouping_column].unique()
2183
2182
  normality_results = []
2184
-
2185
2183
  for column in self.data_column:
2186
2184
  grouped_data = [self.df.loc[self.df[self.grouping_column] == group, column] for group in unique_groups]
2187
2185
  normal_p_values = [normaltest(data).pvalue for data in grouped_data]
2188
2186
  normal_stats = [normaltest(data).statistic for data in grouped_data]
2189
2187
  is_normal = all(p > 0.05 for p in normal_p_values) # Test if all groups are normal
2190
-
2191
2188
  for group, stat, p_value in zip(unique_groups, normal_stats, normal_p_values):
2192
2189
  normality_results.append({
2193
2190
  'Comparison': f'Normality test for {group} on {column}',
@@ -2197,10 +2194,8 @@ class spacrGraph:
2197
2194
  'Column': column,
2198
2195
  'n': len(self.df[self.df[self.grouping_column] == group]) # Sample size
2199
2196
  })
2200
-
2201
2197
  return is_normal, normality_results
2202
2198
 
2203
-
2204
2199
  def perform_levene_test(self, unique_groups):
2205
2200
  """Perform Levene's test for equal variance."""
2206
2201
  grouped_data = [self.df.loc[self.df[self.grouping_column] == group, self.data_column] for group in unique_groups]
@@ -2208,57 +2203,48 @@ class spacrGraph:
2208
2203
  return stat, p_value
2209
2204
 
2210
2205
  def perform_statistical_tests(self, unique_groups, is_normal):
2211
- """Perform statistical tests based on the number of groups, normality, and paired flag."""
2212
- if len(unique_groups) == 2:
2213
- if is_normal:
2214
- if self.paired:
2215
- stat_test = pg.ttest # Paired T-test
2216
- test_name = 'Paired T-test'
2206
+ """Perform statistical tests separately for each data column."""
2207
+ test_results = []
2208
+ for column in self.data_column: # Iterate over each data column
2209
+ grouped_data = [self.df.loc[self.df[self.grouping_column] == group, column] for group in unique_groups]
2210
+ if len(unique_groups) == 2: # For two groups: class_0 vs class_1
2211
+ if is_normal:
2212
+ if self.paired:
2213
+ stat, p = pg.ttest(grouped_data[0], grouped_data[1], paired=True).iloc[0][['T', 'p-val']]
2214
+ test_name = 'Paired T-test'
2215
+ else:
2216
+ stat, p = ttest_ind(grouped_data[0], grouped_data[1])
2217
+ test_name = 'T-test'
2217
2218
  else:
2218
- stat_test = ttest_ind
2219
- test_name = 'T-test'
2219
+ if self.paired:
2220
+ stat, p = pg.wilcoxon(grouped_data[0], grouped_data[1]).iloc[0][['T', 'p-val']]
2221
+ test_name = 'Paired Wilcoxon test'
2222
+ else:
2223
+ stat, p = mannwhitneyu(grouped_data[0], grouped_data[1])
2224
+ test_name = 'Mann-Whitney U test'
2220
2225
  else:
2221
- if self.paired:
2222
- stat_test = pg.wilcoxon # Paired Wilcoxon test
2223
- test_name = 'Paired Wilcoxon test'
2226
+ if is_normal:
2227
+ stat, p = f_oneway(*grouped_data)
2228
+ test_name = 'One-way ANOVA'
2224
2229
  else:
2225
- stat_test = mannwhitneyu
2226
- test_name = 'Mann-Whitney U test'
2227
- else:
2228
- if is_normal:
2229
- stat_test = f_oneway
2230
- test_name = 'One-way ANOVA'
2231
- else:
2232
- stat_test = kruskal
2233
- test_name = 'Kruskal-Wallis test'
2234
-
2235
- comparisons = list(itertools.combinations(unique_groups, 2))
2236
- test_results = []
2237
- for (group1, group2) in comparisons:
2238
- data1 = self.df[self.df[self.grouping_column] == group1][self.data_column]
2239
- data2 = self.df[self.df[self.grouping_column] == group2][self.data_column]
2240
- raw_data1 = self.raw_df[self.raw_df[self.grouping_column] == group1][self.data_column]
2241
- raw_data2 = self.raw_df[self.raw_df[self.grouping_column] == group2][self.data_column]
2242
-
2243
- if self.paired:
2244
- stat, p = stat_test(data1, data2, paired=True)
2245
- else:
2246
- stat, p = stat_test(data1, data2)
2230
+ stat, p = kruskal(*grouped_data)
2231
+ test_name = 'Kruskal-Wallis test'
2247
2232
 
2248
2233
  test_results.append({
2249
- 'Comparison': f'{group1} vs {group2}',
2234
+ 'Comparison': f'{unique_groups[0]} vs {unique_groups[1]} ({column})',
2250
2235
  'Test Statistic': stat,
2251
2236
  'p-value': p,
2252
2237
  'Test Name': test_name,
2253
- 'n_object': len(raw_data1) + len(raw_data2), # Raw sample size (objects/cells)
2254
- 'n_well': len(data1) + len(data2) if self.representation == 'well' else np.nan # Summarized size (wells)
2255
- })
2238
+ 'Column': column,
2239
+ 'n_object': len(grouped_data[0]) + len(grouped_data[1]),
2240
+ 'n_well': len(self.df[self.df[self.grouping_column] == unique_groups[0]]) +
2241
+ len(self.df[self.df[self.grouping_column] == unique_groups[1]])})
2242
+
2256
2243
  return test_results
2257
2244
 
2258
2245
  def perform_posthoc_tests(self, is_normal, unique_groups):
2259
2246
  """Perform post-hoc tests for multiple groups based on all_to_all flag."""
2260
2247
  if is_normal and len(unique_groups) > 2 and self.all_to_all:
2261
- # Tukey HSD Post-hoc when comparing all to all
2262
2248
  tukey_result = pairwise_tukeyhsd(self.df[self.data_column], self.df[self.grouping_column], alpha=0.05)
2263
2249
  posthoc_results = []
2264
2250
  for comparison, p_value in zip(tukey_result._results_table.data[1:], tukey_result.pvalues):
@@ -2271,12 +2257,10 @@ class spacrGraph:
2271
2257
  'p-value': p_value,
2272
2258
  'Test Name': 'Tukey HSD Post-hoc',
2273
2259
  'n_object': len(raw_data1) + len(raw_data2),
2274
- 'n_well': len(self.df[self.df[self.grouping_column] == comparison[0]]) + len(self.df[self.df[self.grouping_column] == comparison[1]])
2275
- })
2260
+ 'n_well': len(self.df[self.df[self.grouping_column] == comparison[0]]) + len(self.df[self.df[self.grouping_column] == comparison[1]])})
2276
2261
  return posthoc_results
2277
2262
 
2278
2263
  elif len(unique_groups) > 2 and not self.all_to_all and self.compare_group:
2279
- # Dunn's post-hoc test using Pingouin
2280
2264
  dunn_result = pg.pairwise_tests(data=self.df, dv=self.data_column, between=self.grouping_column, padjust='bonf', test='dunn')
2281
2265
  posthoc_results = []
2282
2266
  for idx, row in dunn_result.iterrows():
@@ -2287,38 +2271,146 @@ class spacrGraph:
2287
2271
  'p-value': row['p-val'],
2288
2272
  'Test Name': 'Dunn’s Post-hoc',
2289
2273
  'n_object': None,
2290
- 'n_well': None
2291
- })
2274
+ 'n_well': None})
2275
+
2292
2276
  return posthoc_results
2293
2277
  return []
2294
-
2278
+
2295
2279
  def create_plot(self, ax=None):
2296
2280
  """Create and display the plot based on the chosen graph type."""
2281
+
2282
+ def _generate_tabels(unique_groups):
2283
+ """Generate row labels and a symbol table for multi-level grouping."""
2284
+ # Create row labels: Include the grouping column and data columns
2285
+ row_labels = [self.grouping_column] + self.data_column
2286
+
2287
+ # Initialize table data
2288
+ table_data = []
2289
+
2290
+ # Create the grouping row: Alternate each group for every data column
2291
+ grouping_row = []
2292
+ for _ in self.data_column:
2293
+ for group in unique_groups:
2294
+ grouping_row.append(group)
2295
+ table_data.append(grouping_row) # Add the grouping row to the table
2296
+
2297
+ # Create symbol rows for each data column
2298
+ for column in self.data_column:
2299
+ column_row = [] # Initialize a row for this column
2300
+ for data_col in self.data_column: # Iterate over data columns to align with the structure
2301
+ for group in unique_groups:
2302
+ # Assign '+' if the column matches, otherwise assign '-'
2303
+ if column == data_col:
2304
+ column_row.append('+')
2305
+ else:
2306
+ column_row.append('-')
2307
+ table_data.append(column_row) # Add this row to the table
2308
+
2309
+ # Transpose the table to align with the plot layout
2310
+ transposed_table = list(map(list, zip(*table_data)))
2311
+ return row_labels, transposed_table
2312
+
2313
+ def _place_symbols(row_labels, transposed_table, x_positions, ax):
2314
+
2315
+ # Get the bottom of the y-axis (y=0) in data coordinates and convert to display coordinates
2316
+ y_axis_min = ax.get_ylim()[0] # Minimum y-axis value (usually 0)
2317
+ symbol_start_y = ax.transData.transform((0, y_axis_min))[1] - 30 # Slightly below the x-axis line
2318
+
2319
+ # Convert to figure coordinates
2320
+ symbol_start_y_fig = ax.transAxes.inverted().transform((0, symbol_start_y))[1]
2321
+
2322
+ # Calculate y-spacing for the table rows (adjust as needed)
2323
+ y_spacing = 0.02 # Control vertical spacing between elements
2324
+
2325
+ # X-coordinate for the row labels at the y-axis and x-axis intersection
2326
+ label_x_pos = ax.get_xlim()[0] - 0.5 # Slightly offset from the y-axis
2327
+
2328
+ # Place the row titles at the y-axis intersection
2329
+ for row_idx, title in enumerate(row_labels):
2330
+ y_pos = symbol_start_y_fig - (row_idx * y_spacing) # Align with row index
2331
+ ax.text(label_x_pos, y_pos, title, ha='right', va='center', fontsize=12, fontweight='regular')
2332
+
2333
+ # Place the symbols under each bar
2334
+ for idx, (x_pos, column_data) in enumerate(zip(x_positions, transposed_table)):
2335
+ for row_idx, text in enumerate(column_data):
2336
+ y_pos = symbol_start_y_fig - (row_idx * y_spacing)
2337
+ ax.text(x_pos, y_pos, text, ha='center', va='center', fontsize=12)
2338
+
2339
+ def _get_positions(self, ax):
2340
+ if self.graph_type == 'bar':
2341
+ x_positions = [np.mean(bar.get_paths()[0].vertices[:, 0]) for bar in ax.collections if hasattr(bar, 'get_paths')]
2342
+
2343
+ elif self.graph_type == 'violin':
2344
+ x_positions = [np.mean(violin.get_paths()[0].vertices[:, 0]) for violin in ax.collections if hasattr(violin, 'get_paths')]
2345
+
2346
+ elif self.graph_type == 'box':
2347
+ x_positions = list(set(line.get_xdata().mean() for line in ax.lines if line.get_linestyle() == '-'))
2348
+
2349
+ elif self.graph_type == 'jitter':
2350
+ x_positions = [np.mean(collection.get_offsets()[:, 0]) for collection in ax.collections if collection.get_offsets().size > 0]
2351
+ return x_positions
2352
+
2353
+ def _draw_comparison_lines(ax, x_positions):
2354
+ """Draw comparison lines and annotate significance based on results_df."""
2355
+ if self.results_df.empty:
2356
+ print("No comparisons available to annotate.")
2357
+ return
2358
+
2359
+ y_max = max([bar.get_height() for bar in ax.patches])
2360
+ ax.set_ylim(0, y_max * 1.3)
2361
+
2362
+ for idx, row in self.results_df.iterrows():
2363
+ group1, group2 = row['Comparison'].split(' vs ')
2364
+ p_value = row['p-value']
2365
+
2366
+ # Determine significance marker
2367
+ if p_value <= 0.001:
2368
+ significance = '***'
2369
+ elif p_value <= 0.01:
2370
+ significance = '**'
2371
+ elif p_value <= 0.05:
2372
+ significance = '*'
2373
+ else:
2374
+ significance = 'ns'
2375
+
2376
+ # Find the x positions of the compared groups
2377
+ x1 = x_positions[unique_groups.tolist().index(group1)]
2378
+ x2 = x_positions[unique_groups.tolist().index(group2)]
2379
+
2380
+ # Stagger lines to avoid overlap
2381
+ line_y = y_max + (0.1 * y_max) * (idx + 1)
2382
+
2383
+ # Draw the comparison line
2384
+ ax.plot([x1, x1, x2, x2], [line_y - 0.02, line_y, line_y, line_y - 0.02], lw=1.5, c='black')
2385
+
2386
+ # Add the significance marker
2387
+ ax.text((x1 + x2) / 2, line_y, significance, ha='center', va='bottom', fontsize=12)
2388
+
2297
2389
  # Optional: Remove outliers for plotting
2298
2390
  if self.remove_outliers:
2299
2391
  self.df = self.remove_outliers_from_plot()
2300
2392
 
2301
- # Get the unique groups from the grouping column
2393
+ self.df_melted = pd.melt(self.df, id_vars=[self.grouping_column], value_vars=self.data_column,var_name='Data Column', value_name='Value')
2302
2394
  unique_groups = self.df[self.grouping_column].unique()
2395
+ is_normal, normality_results = self.perform_normality_tests()
2396
+ levene_stat, levene_p = self.perform_levene_test(unique_groups)
2397
+ test_results = self.perform_statistical_tests(unique_groups, is_normal)
2398
+ posthoc_results = self.perform_posthoc_tests(is_normal, unique_groups)
2399
+ self.results_df = pd.DataFrame(normality_results + test_results + posthoc_results)
2303
2400
 
2304
- # Flatten the DataFrame to handle multiple `data_column` values in a single plot
2305
- self.df_melted = pd.melt(self.df, id_vars=[self.grouping_column], value_vars=self.data_column,
2306
- var_name='Data Column', value_name='Value')
2307
-
2308
- # Dynamically set figure dimensions based on the number of unique groups and data columns
2309
- num_groups = len(self.df_melted[self.grouping_column].unique())
2401
+ num_groups = len(self.data_column)*len(self.grouping_column)
2310
2402
  num_data_columns = len(self.data_column)
2311
- bar_width = 2.0 / num_data_columns
2312
- spacing_between_groups = 0.1
2313
-
2314
- fig_width = (num_groups * num_data_columns * bar_width) + (spacing_between_groups * num_groups)
2315
- fig_height = 10
2403
+ self.bar_width = 0.4
2404
+ spacing_between_groups = self.bar_width/0.5
2316
2405
 
2406
+ self.fig_width = (num_groups * self.bar_width) + (spacing_between_groups * num_groups)
2407
+ self.fig_height = self.fig_width/2
2408
+
2317
2409
  if ax is None:
2318
- self.fig, ax = plt.subplots(figsize=(fig_width, fig_height))
2410
+ self.fig, ax = plt.subplots(figsize=(self.fig_height, self.fig_width))
2319
2411
  else:
2320
2412
  self.fig = ax.figure
2321
-
2413
+
2322
2414
  if len(self.data_column) == 1:
2323
2415
  self.hue=self.grouping_column
2324
2416
  self.jitter_bar_dodge = False
@@ -2328,9 +2420,8 @@ class spacrGraph:
2328
2420
 
2329
2421
  # Handle the different plot types based on `graph_type`
2330
2422
  if self.graph_type == 'bar':
2331
- self._create_bar_plot(bar_width, ax)
2423
+ self._create_bar_plot(ax)
2332
2424
  elif self.graph_type == 'jitter':
2333
- #transparent_palette = [(r, g, b, 0.6) for (r, g, b) in sns.color_palette(self.sns_palette, n_colors=len(df_melted[hue].unique()))]
2334
2425
  self._create_jitter_plot(ax)
2335
2426
  elif self.graph_type == 'box':
2336
2427
  self._create_box_plot(ax)
@@ -2339,159 +2430,154 @@ class spacrGraph:
2339
2430
  else:
2340
2431
  raise ValueError(f"Unknown graph type: {self.graph_type}")
2341
2432
 
2342
- if ax is None:
2343
- self.fig, ax = plt.subplots(figsize=(fig_width, fig_height))
2344
- else:
2345
- self.fig = ax.figure
2346
-
2347
-
2348
2433
  # Set y-axis start
2349
- if self.y_axis_start is not None:
2350
- ax.set_ylim(bottom=self.y_axis_start)
2434
+ if isinstance(self.y_lim, list):
2435
+ if len(self.y_lim) == 2:
2436
+ ax.set_ylim(self.y_lim[0], self.y_lim[1])
2437
+ elif len(self.y_lim) == 1:
2438
+ ax.set_ylim(self.y_lim[0], None)
2351
2439
 
2352
- # Remove top and right spines
2353
2440
  sns.despine(ax=ax, top=True, right=True)
2354
-
2355
- # Adjust the position of the plot to move it closer to the table
2356
- ax.set_position([0.1, 0.25, 0.8, 0.45]) # Adjust the height of the plot, leaving more space for the table
2357
-
2358
- # Move the legend outside the plot
2359
- ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), title='Data Column')
2360
-
2441
+ ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), title='Data Column') # Move the legend outside the plot
2361
2442
  ax.set_xlabel('')
2443
+ x_positions = _get_positions(self, ax)
2362
2444
 
2363
2445
  if len(self.data_column) == 1:
2364
2446
  ax.legend().remove()
2365
2447
  ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right')
2366
2448
 
2367
- # Create the grid with '+' and '-' symbols
2368
- if len(self.data_column) > 1:
2449
+ elif len(self.data_column) > 1:
2369
2450
  ax.set_xticks([])
2370
2451
  ax.tick_params(bottom=False)
2371
2452
  ax.set_xticklabels([])
2372
-
2373
- legend_ax = self.fig.add_axes([0.1, 0.02, 0.8, 0.2]) # Position the table closer to the graph
2453
+ legend_ax = self.fig.add_axes([0.1, -0.2, 0.62, 0.2]) # Position the table closer to the graph
2374
2454
  legend_ax.set_axis_off()
2375
2455
 
2376
- # Prepare the rows and symbols
2377
- row_labels = self.data_column
2378
- table_data = []
2379
-
2380
- # Ensure the rows for each data column match the number of unique bars
2381
- for column in self.data_column:
2382
- column_bars = []
2383
- for group in unique_groups:
2384
- # For each group and data column, assign a '+' or '-'
2385
- for bar_position in range(num_data_columns): # Loop through each data_column position
2386
- if bar_position == self.data_column.index(column):
2387
- column_bars.append('+')
2388
- else:
2389
- column_bars.append('-')
2390
- table_data.append(column_bars)
2391
-
2392
- # Display the table directly below the graph with white background and thicker row height
2393
- legend_table = legend_ax.table(cellText=table_data,
2394
- rowLabels=row_labels,
2395
- loc='center',
2396
- cellLoc='center',
2397
- rowColours=['white'] * len(row_labels), # Set background to white
2398
- cellColours=[['white'] * len(column_bars) for _ in table_data], # White cell background
2399
- edges='closed', # Show edges around cells
2400
- bbox=[0, 0, 1, 1])
2401
-
2402
- # Adjust the row height for readability
2403
- for key, cell in legend_table.get_celld().items():
2404
- cell.set_height(0.5) # Increase row height
2405
- cell.set_edgecolor('white') # Set grid color to white
2406
- cell.set_linewidth(2) # Set line thickness
2407
-
2456
+ row_labels, table_data = _generate_tabels(unique_groups)
2457
+ _place_symbols(row_labels, table_data, x_positions, ax)
2458
+
2459
+ #_draw_comparison_lines(ax, x_positions)
2460
+
2408
2461
  if self.save:
2409
2462
  self._save_results()
2410
2463
 
2411
- plt.tight_layout() # Ensure the layout is tight
2412
- plt.show() # Ensure the plot is shown, but plt.show() doesn't clear the figure context
2413
-
2414
- def get_figure(self):
2415
- """Return the generated figure."""
2416
- return self.fig
2464
+ ax.margins(x=0.12)
2417
2465
 
2418
- def _create_bar_plot(self, bar_width, ax):
2466
+ def _create_bar_plot(self, ax):
2419
2467
  """Helper method to create a bar plot with consistent bar thickness and centered error bars."""
2420
-
2421
- # Melt the dataframe for easier use with seaborn's hue functionality
2422
- df_melted = pd.melt(self.df, id_vars=[self.grouping_column], value_vars=self.data_column,
2423
- var_name='Data Column', value_name=self.summary_func)
2424
-
2425
- # Group the melted DataFrame by grouping column and data column for error bars calculation
2426
- summary_df = df_melted.groupby([self.grouping_column, 'Data Column']).agg({self.summary_func: ['mean', 'std', 'sem']}).reset_index()
2427
- summary_df.columns = ['_'.join(col).strip() if isinstance(col, tuple) else col for col in summary_df.columns]
2428
-
2429
- # Determine which type of error bars to show (std or sem)
2430
- if self.error_bar_type == 'std':
2431
- error_bars = summary_df[f'{self.summary_func}_std']
2432
- elif self.error_bar_type == 'sem':
2433
- error_bars = summary_df[f'{self.summary_func}_sem']
2468
+ # Flatten DataFrame: Combine grouping column and data column into one group if needed
2469
+ if len(self.data_column) > 1:
2470
+ self.df_melted['Combined Group'] = (self.df_melted[self.grouping_column].astype(str) + " - " + self.df_melted['Data Column'].astype(str))
2471
+ x_axis_column = 'Combined Group'
2472
+ hue = None
2473
+ ax.set_ylabel('Value')
2434
2474
  else:
2435
- raise ValueError(f"Invalid error_bar_type: {self.error_bar_type}. Choose either 'std' or 'sem'.")
2436
-
2437
- # Set the hue to 'Data Column' if there are multiple data columns
2438
- hue = 'Data Column' if len(self.data_column) > 1 else None
2439
-
2440
- # Create the bar plot with dodge=True to ensure bars for different data columns are side by side
2441
- barplot = sns.barplot(
2442
- data=df_melted,
2443
- x=self.grouping_column,
2444
- y=self.summary_func,
2445
- hue=hue,
2446
- palette=self.sns_palette,
2447
- dodge=self.jitter_bar_dodge, # Ensure bars are separated
2448
- ax=ax,
2449
- ci=None # Disable Seaborn's internal error bars
2450
- )
2451
-
2452
- # Sort summary_df to match the order of bars in the plot
2453
- summary_df_sorted = summary_df.sort_values([f'{self.grouping_column}_', 'Data Column_'])
2454
-
2455
- # Get the positions of the bars
2456
- bars = [patch for patch in ax.patches if isinstance(patch, plt.Rectangle)]
2457
-
2458
- # Ensure error bars are aligned with correct bars
2459
- for bar, (_, row) in zip(bars, summary_df_sorted.iterrows()):
2460
- x_bar = bar.get_x() + bar.get_width() / 2 # Center of the bar
2461
- err = row[f'{self.summary_func}_{self.error_bar_type}'] # Get the correct error value for this bar
2462
-
2475
+ x_axis_column = self.grouping_column
2476
+ ax.set_ylabel(self.data_column[0])
2477
+ hue = None
2478
+
2479
+ summary_df = self.df_melted.groupby([x_axis_column]).agg(mean=('Value', 'mean'),std=('Value', 'std'),sem=('Value', 'sem')).reset_index()
2480
+ error_bars = summary_df[self.error_bar_type] if self.error_bar_type in ['std', 'sem'] else None
2481
+ sns.barplot(data=self.df_melted, x=x_axis_column, y='Value', hue=self.hue, palette=self.sns_palette, ax=ax, dodge=self.jitter_bar_dodge, ci=None)
2482
+
2483
+ # Adjust the bar width manually
2484
+ if len(self.data_column) > 1:
2485
+ bars = [bar for bar in ax.patches if isinstance(bar, plt.Rectangle)]
2486
+ target_width = self.bar_width * 2
2487
+ for bar in bars:
2488
+ bar.set_width(target_width) # Set new width
2489
+ # Center the bar on its x-coordinate
2490
+ bar.set_x(bar.get_x() - target_width / 2)
2491
+
2492
+ # Adjust error bars alignment with bars
2493
+ bars = [bar for bar in ax.patches if isinstance(bar, plt.Rectangle)]
2494
+ for bar, (_, row) in zip(bars, summary_df.iterrows()):
2495
+ x_bar = bar.get_x() + bar.get_width() / 2
2496
+ err = row[self.error_bar_type]
2463
2497
  ax.errorbar(x=x_bar, y=bar.get_height(), yerr=err, fmt='none', c='black', capsize=5, lw=2)
2464
-
2465
- # Set the legend outside the plot
2466
- if hue:
2467
- ax.legend(title="Data Column", loc='center left', bbox_to_anchor=(1, 0.5))
2468
- else:
2469
- if len(self.data_column) > 1:
2470
- ax.legend_.remove()
2471
-
2472
- # Set labels
2498
+
2499
+ # Set legend and labels
2473
2500
  ax.set_xlabel(self.grouping_column)
2474
- ax.set_ylabel(self.summary_func)
2475
-
2476
2501
 
2477
2502
  def _create_jitter_plot(self, ax):
2478
- """Helper method to create a jitter plot (strip plot) for a specified column."""
2479
- sns.stripplot(data=self.df_melted, x=self.grouping_column, y='Value', hue=self.hue, palette=self.sns_palette, dodge=self.jitter_bar_dodge, jitter=True, ax=ax, alpha=0.6)
2480
-
2503
+ """Helper method to create a jitter plot (strip plot) with consistent spacing."""
2504
+ # Combine grouping column and data column if needed
2505
+ if len(self.data_column) > 1:
2506
+ self.df_melted['Combined Group'] = (self.df_melted[self.grouping_column].astype(str) + " - " + self.df_melted['Data Column'].astype(str))
2507
+ x_axis_column = 'Combined Group'
2508
+ hue = None # Disable hue to avoid two-level grouping
2509
+ ax.set_ylabel('Value')
2510
+ else:
2511
+ x_axis_column = self.grouping_column
2512
+ ax.set_ylabel(self.data_column[0])
2513
+ hue = None
2514
+
2515
+ # Create the jitter plot
2516
+ sns.stripplot(data=self.df_melted,x=x_axis_column,y='Value',hue=self.hue, palette=self.sns_palette, dodge=self.jitter_bar_dodge, jitter=self.bar_width, ax=ax,alpha=0.6)
2517
+
2518
+ # Adjust legend and labels
2519
+ ax.set_xlabel(self.grouping_column)
2520
+
2521
+ # Manage the legend
2522
+ handles, labels = ax.get_legend_handles_labels()
2523
+ unique_labels = dict(zip(labels, handles))
2524
+ ax.legend(unique_labels.values(), unique_labels.keys(), loc='best')
2525
+
2481
2526
  def _create_box_plot(self, ax):
2482
- """Helper method to create a box plot for a specified column."""
2483
- sns.boxplot(data=self.df_melted, x=self.grouping_column, y='Value', hue=self.hue, palette=self.sns_palette, ax=ax)
2527
+ """Helper method to create a box plot with consistent spacing."""
2528
+ # Combine grouping column and data column if needed
2529
+ if len(self.data_column) > 1:
2530
+ self.df_melted['Combined Group'] = (self.df_melted[self.grouping_column].astype(str) + " - " + self.df_melted['Data Column'].astype(str))
2531
+ x_axis_column = 'Combined Group'
2532
+ hue = None
2533
+ ax.set_ylabel('Value')
2534
+ else:
2535
+ x_axis_column = self.grouping_column
2536
+ ax.set_ylabel(self.data_column[0])
2537
+ hue = None
2538
+
2539
+ # Create the box plot
2540
+ sns.boxplot(data=self.df_melted,x=x_axis_column,y='Value',hue=self.hue,palette=self.sns_palette,ax=ax)
2541
+
2542
+ # Adjust legend and labels
2543
+ ax.set_xlabel(self.grouping_column)
2484
2544
 
2545
+ # Manage the legend
2546
+ handles, labels = ax.get_legend_handles_labels()
2547
+ unique_labels = dict(zip(labels, handles))
2548
+ ax.legend(unique_labels.values(), unique_labels.keys(), loc='best')
2549
+
2485
2550
  def _create_violin_plot(self, ax):
2486
- """Helper method to create a violin plot for a specified column."""
2487
- sns.violinplot(data=self.df_melted, x=self.grouping_column, y='Value', hue=self.hue, palette=self.sns_palette, ax=ax)
2551
+ """Helper method to create a violin plot with consistent spacing."""
2552
+ # Combine grouping column and data column if needed
2553
+ if len(self.data_column) > 1:
2554
+ self.df_melted['Combined Group'] = (self.df_melted[self.grouping_column].astype(str) + " - " + self.df_melted['Data Column'].astype(str))
2555
+ x_axis_column = 'Combined Group'
2556
+ hue = None
2557
+ ax.set_ylabel('Value')
2558
+ else:
2559
+ x_axis_column = self.grouping_column
2560
+ ax.set_ylabel(self.data_column[0])
2561
+ hue = None
2562
+
2563
+ # Create the violin plot
2564
+ sns.violinplot(data=self.df_melted,x=x_axis_column,y='Value', hue=self.hue,palette=self.sns_palette,ax=ax)
2565
+
2566
+ # Adjust legend and labels
2567
+ ax.set_xlabel(self.grouping_column)
2568
+ ax.set_ylabel('Value')
2569
+
2570
+ # Manage the legend
2571
+ handles, labels = ax.get_legend_handles_labels()
2572
+ unique_labels = dict(zip(labels, handles))
2573
+ ax.legend(unique_labels.values(), unique_labels.keys(), loc='best')
2488
2574
 
2489
2575
  def _save_results(self):
2490
2576
  """Helper method to save the plot and results."""
2491
2577
  os.makedirs(self.output_dir, exist_ok=True)
2492
- plot_path = os.path.join(self.output_dir, 'grouped_plot.png')
2493
- self.fig.savefig(plot_path)
2494
- results_path = os.path.join(self.output_dir, 'test_results.csv')
2578
+ plot_path = os.path.join(self.output_dir, f"{self.results_name}.pdf")
2579
+ self.fig.savefig(plot_path, bbox_inches='tight', dpi=600, transparent=True, format='pdf')
2580
+ results_path = os.path.join(self.output_dir, f"{self.results_name}.csv")
2495
2581
  self.results_df.to_csv(results_path, index=False)
2496
2582
  print(f"Plot saved to {plot_path}")
2497
2583
  print(f"Test results saved to {results_path}")
@@ -2500,9 +2586,13 @@ class spacrGraph:
2500
2586
  """Return the results dataframe."""
2501
2587
  return self.results_df
2502
2588
 
2589
+ def get_figure(self):
2590
+ """Return the generated figure."""
2591
+ return self.fig
2592
+
2503
2593
  def plot_data_from_db(settings):
2504
2594
  from .io import _read_db
2505
- from spacr.utils import annotate_conditions
2595
+ from .utils import annotate_conditions
2506
2596
  """
2507
2597
  Extracts the specified table from the SQLite database and plots a specified column.
2508
2598
 
@@ -2530,18 +2620,17 @@ def plot_data_from_db(settings):
2530
2620
  df['prc'] = df['plate'].astype(str) + '_' + df['row'].astype(str) + '_' + df['col'].astype(str)
2531
2621
  df = df.dropna(subset=settings['column_name'])
2532
2622
  df['class'] = df['png_path'].apply(lambda x: 'class_1' if 'class_1' in x else ('class_0' if 'class_0' in x else None))
2533
- #display(df)
2534
- # Initialize the spacrGraph class with your DataFrame and desired parameters
2623
+
2535
2624
  spacr_graph = spacrGraph(
2536
- df=df, # Your DataFrame
2625
+ df=df, # Your DataFrame
2537
2626
  grouping_column=settings['grouping_column'], # Column for grouping the data (x-axis)
2538
2627
  data_column=settings['column_name'], # Column for the data (y-axis)
2539
2628
  graph_type=settings['graph_type'], # Type of plot ('bar', 'box', 'violin', 'jitter')
2540
2629
  summary_func='mean', # Function to summarize data (e.g., 'mean', 'median')
2541
2630
  colors=None, # Custom colors for the plot (optional)
2542
- output_dir=settings['dst'], # Directory to save the plot and results
2543
- save=settings['save'], # Whether to save the plot and results
2544
- y_axis_start=0, # Starting point for y-axis (optional)
2631
+ output_dir=settings['dst'], # Directory to save the plot and results
2632
+ save=settings['save'], # Whether to save the plot and results
2633
+ y_lim=settings['y_lim'], # Starting point for y-axis (optional)
2545
2634
  error_bar_type='std', # Type of error bar ('std' or 'sem')
2546
2635
  representation='well',
2547
2636
  theme=settings['theme'], # Seaborn color palette theme (e.g., 'pastel', 'muted')
@@ -2552,8 +2641,9 @@ def plot_data_from_db(settings):
2552
2641
 
2553
2642
  # Get the figure object if needed
2554
2643
  fig = spacr_graph.get_figure()
2644
+ plt.show()
2555
2645
 
2556
2646
  # Optional: Get the results DataFrame containing statistical test results
2557
2647
  results_df = spacr_graph.get_results()
2558
- display(results_df)
2559
- return fig
2648
+
2649
+ return fig, results_df
spacr/settings.py CHANGED
@@ -278,6 +278,7 @@ def get_measure_crop_settings(settings={}):
278
278
 
279
279
  def set_default_analyze_screen(settings):
280
280
  settings.setdefault('src', 'path')
281
+ settings.setdefault('annotation_column', None)
281
282
  settings.setdefault('model_type_ml','xgboost')
282
283
  settings.setdefault('heatmap_feature','predictions')
283
284
  settings.setdefault('grouping','mean')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spacr
3
- Version: 0.3.33
3
+ Version: 0.3.34
4
4
  Summary: Spatial phenotype analysis of crisp screens (SpaCr)
5
5
  Home-page: https://github.com/EinarOlafsson/spacr
6
6
  Author: Einar Birnir Olafsson
@@ -13,16 +13,16 @@ spacr/deep_spacr.py,sha256=HdOcNU8cHcE_19nP7_5uTz-ih3E169ffr2Hm--NvMvA,43255
13
13
  spacr/gui.py,sha256=ARyn9Q_g8HoP-cXh1nzMLVFCKqthY4v2u9yORyaQqQE,8230
14
14
  spacr/gui_core.py,sha256=LV_HX5zreu3Bye6sQFDbOuk8Dfj4StMoohy6hsrDEXA,41363
15
15
  spacr/gui_elements.py,sha256=puDqf7PJJ_UMA01fjqODk-zsfSmvzVXpvaZ1BYV988w,136554
16
- spacr/gui_utils.py,sha256=cZ_EEr1SE1zJFLmYrDJB896u_reTL_FJbP6fSY5Xx6U,45454
16
+ spacr/gui_utils.py,sha256=7e9DsZIuV7-jh97kEf7v1In_cFzlFueV4SGcGYGpTxw,45454
17
17
  spacr/io.py,sha256=AARmqn1fMmTgVDwWy8bEYK6SjH-6DZIulgCSPdBTyf0,143370
18
18
  spacr/logger.py,sha256=lJhTqt-_wfAunCPl93xE65Wr9Y1oIHJWaZMjunHUeIw,1538
19
19
  spacr/measure.py,sha256=BThn_sALgKrwGKnLOGpT4FyoJeRVoTZoP9SXbCtCMRw,54857
20
20
  spacr/mediar.py,sha256=FwLvbLQW5LQzPgvJZG8Lw7GniA2vbZx6Jv6vIKu7I5c,14743
21
- spacr/ml.py,sha256=3XiQUfhhseCz9cZXhaVkCCv_qfqoZCdXGnO_p3ulwo4,47131
21
+ spacr/ml.py,sha256=8i2D9YEC9rSYdbgkuuMLl6adwivWn2Z5BEUjPRsW4t4,48983
22
22
  spacr/openai.py,sha256=5vBZ3Jl2llYcW3oaTEXgdyCB2aJujMUIO5K038z7w_A,1246
23
- spacr/plot.py,sha256=7ZeiOlFHDD3Yp9_qrCycEpnuaFVPvRwXgQH_h7YZ94E,111792
23
+ spacr/plot.py,sha256=PtCSoBmLFlGC7ebmsk-vMlyd7q2ahXgRVaTtAq3w_po,116513
24
24
  spacr/sequencing.py,sha256=t18mgpK6rhWuB1LtFOsPxqgpFXxuUmrD06ecsaVQ0Gw,19655
25
- spacr/settings.py,sha256=uTTR6pmwBHbZ_uLLWE4cXplGK7q6K_OmZnsXH-HAFW0,75828
25
+ spacr/settings.py,sha256=7rAvzPkfyfbpY6JQqcTe6PcCghEvLebUgsfKMVBtNyU,75879
26
26
  spacr/sim.py,sha256=1xKhXimNU3ukzIw-3l9cF3Znc_brW8h20yv8fSTzvss,71173
27
27
  spacr/submodules.py,sha256=AB7s6-cULsaqz-haAaCtXfGEIi8uPZGT4xoCslUJC3Y,18391
28
28
  spacr/timelapse.py,sha256=FSYpUtAVy6xc3lwprRYgyDTT9ysUhfRQ4zrP9_h2mvg,39465
@@ -150,9 +150,9 @@ spacr/resources/icons/umap.png,sha256=dOLF3DeLYy9k0nkUybiZMe1wzHQwLJFRmgccppw-8b
150
150
  spacr/resources/images/plate1_E01_T0001F001L01A01Z01C02.tif,sha256=Tl0ZUfZ_AYAbu0up_nO0tPRtF1BxXhWQ3T3pURBCCRo,7958528
151
151
  spacr/resources/images/plate1_E01_T0001F001L01A02Z01C01.tif,sha256=m8N-V71rA1TT4dFlENNg8s0Q0YEXXs8slIn7yObmZJQ,7958528
152
152
  spacr/resources/images/plate1_E01_T0001F001L01A03Z01C03.tif,sha256=Pbhk7xn-KUP6RSIhJsxQcrHFImBm3GEpLkzx7WOc-5M,7958528
153
- spacr-0.3.33.dist-info/LICENSE,sha256=SR-2MeGc6SCM1UORJYyarSWY_A-JaOMFDj7ReSs9tRM,1083
154
- spacr-0.3.33.dist-info/METADATA,sha256=sO_89AozvIDEHWwzCw9s8zfwlWGQLLdgrbyC-PVKKTY,5949
155
- spacr-0.3.33.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
156
- spacr-0.3.33.dist-info/entry_points.txt,sha256=BMC0ql9aNNpv8lUZ8sgDLQMsqaVnX5L535gEhKUP5ho,296
157
- spacr-0.3.33.dist-info/top_level.txt,sha256=GJPU8FgwRXGzKeut6JopsSRY2R8T3i9lDgya42tLInY,6
158
- spacr-0.3.33.dist-info/RECORD,,
153
+ spacr-0.3.34.dist-info/LICENSE,sha256=SR-2MeGc6SCM1UORJYyarSWY_A-JaOMFDj7ReSs9tRM,1083
154
+ spacr-0.3.34.dist-info/METADATA,sha256=6C9_x3YSb9ycM9cXRufcPTJG809Cf-hkNYDOdEVXMT0,5949
155
+ spacr-0.3.34.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
156
+ spacr-0.3.34.dist-info/entry_points.txt,sha256=BMC0ql9aNNpv8lUZ8sgDLQMsqaVnX5L535gEhKUP5ho,296
157
+ spacr-0.3.34.dist-info/top_level.txt,sha256=GJPU8FgwRXGzKeut6JopsSRY2R8T3i9lDgya42tLInY,6
158
+ spacr-0.3.34.dist-info/RECORD,,
File without changes