spacr 0.3.32__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,22 +2096,23 @@ 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
- self.data_column = data_column
2109
+ self.data_column = data_column if isinstance(data_column, list) else [data_column]
2107
2110
  self.graph_type = graph_type
2108
2111
  self.summary_func = summary_func
2109
2112
  self.order = order
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):
@@ -2148,17 +2151,15 @@ class spacrGraph:
2148
2151
 
2149
2152
  def preprocess_data(self):
2150
2153
  """Preprocess the data: remove NaNs, sort/order the grouping column, and optionally group by 'prc'."""
2151
- df = self.df.dropna(subset=[self.grouping_column, self.data_column])
2152
-
2154
+ # Remove NaNs in both the grouping column and each data column
2155
+ df = self.df.dropna(subset=[self.grouping_column] + self.data_column) # Handle multiple data columns
2153
2156
  # Group by 'prc' column if representation is 'well'
2154
2157
  if self.representation == 'well':
2155
2158
  df = df.groupby(['prc', self.grouping_column])[self.data_column].agg(self.summary_func).reset_index()
2156
-
2157
2159
  if self.order:
2158
2160
  df[self.grouping_column] = pd.Categorical(df[self.grouping_column], categories=self.order, ordered=True)
2159
2161
  else:
2160
2162
  df[self.grouping_column] = pd.Categorical(df[self.grouping_column], categories=sorted(df[self.grouping_column].unique()), ordered=True)
2161
-
2162
2163
  return df
2163
2164
 
2164
2165
  def remove_outliers_from_plot(self):
@@ -2176,26 +2177,24 @@ class spacrGraph:
2176
2177
  return filtered_df
2177
2178
 
2178
2179
  def perform_normality_tests(self):
2179
- """Perform normality tests for each group."""
2180
+ """Perform normality tests for each group and each data column."""
2180
2181
  unique_groups = self.df[self.grouping_column].unique()
2181
- grouped_data = [self.df.loc[self.df[self.grouping_column] == group, self.data_column] for group in unique_groups]
2182
- raw_grouped_data = [self.raw_df.loc[self.raw_df[self.grouping_column] == group, self.data_column] for group in unique_groups]
2183
-
2184
- normal_p_values = [normaltest(data).pvalue for data in grouped_data]
2185
- normal_stats = [normaltest(data).statistic for data in grouped_data]
2186
- is_normal = all(p > 0.05 for p in normal_p_values)
2187
-
2188
- test_results = []
2189
- for group, stat, p_value in zip(unique_groups, normal_stats, normal_p_values):
2190
- test_results.append({
2191
- 'Comparison': f'Normality test for {group}',
2192
- 'Test Statistic': stat,
2193
- 'p-value': p_value,
2194
- 'Test Name': 'Normality test',
2195
- 'n_object': len(raw_grouped_data[unique_groups.tolist().index(group)]), # Raw sample size (objects/cells)
2196
- 'n_well': len(grouped_data[unique_groups.tolist().index(group)]) if self.representation == 'well' else np.nan # Summarized size (wells)
2197
- })
2198
- return is_normal, test_results
2182
+ normality_results = []
2183
+ for column in self.data_column:
2184
+ grouped_data = [self.df.loc[self.df[self.grouping_column] == group, column] for group in unique_groups]
2185
+ normal_p_values = [normaltest(data).pvalue for data in grouped_data]
2186
+ normal_stats = [normaltest(data).statistic for data in grouped_data]
2187
+ is_normal = all(p > 0.05 for p in normal_p_values) # Test if all groups are normal
2188
+ for group, stat, p_value in zip(unique_groups, normal_stats, normal_p_values):
2189
+ normality_results.append({
2190
+ 'Comparison': f'Normality test for {group} on {column}',
2191
+ 'Test Statistic': stat,
2192
+ 'p-value': p_value,
2193
+ 'Test Name': 'Normality test',
2194
+ 'Column': column,
2195
+ 'n': len(self.df[self.df[self.grouping_column] == group]) # Sample size
2196
+ })
2197
+ return is_normal, normality_results
2199
2198
 
2200
2199
  def perform_levene_test(self, unique_groups):
2201
2200
  """Perform Levene's test for equal variance."""
@@ -2204,57 +2203,48 @@ class spacrGraph:
2204
2203
  return stat, p_value
2205
2204
 
2206
2205
  def perform_statistical_tests(self, unique_groups, is_normal):
2207
- """Perform statistical tests based on the number of groups, normality, and paired flag."""
2208
- if len(unique_groups) == 2:
2209
- if is_normal:
2210
- if self.paired:
2211
- stat_test = pg.ttest # Paired T-test
2212
- 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'
2213
2218
  else:
2214
- stat_test = ttest_ind
2215
- 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'
2216
2225
  else:
2217
- if self.paired:
2218
- stat_test = pg.wilcoxon # Paired Wilcoxon test
2219
- test_name = 'Paired Wilcoxon test'
2226
+ if is_normal:
2227
+ stat, p = f_oneway(*grouped_data)
2228
+ test_name = 'One-way ANOVA'
2220
2229
  else:
2221
- stat_test = mannwhitneyu
2222
- test_name = 'Mann-Whitney U test'
2223
- else:
2224
- if is_normal:
2225
- stat_test = f_oneway
2226
- test_name = 'One-way ANOVA'
2227
- else:
2228
- stat_test = kruskal
2229
- test_name = 'Kruskal-Wallis test'
2230
-
2231
- comparisons = list(itertools.combinations(unique_groups, 2))
2232
- test_results = []
2233
- for (group1, group2) in comparisons:
2234
- data1 = self.df[self.df[self.grouping_column] == group1][self.data_column]
2235
- data2 = self.df[self.df[self.grouping_column] == group2][self.data_column]
2236
- raw_data1 = self.raw_df[self.raw_df[self.grouping_column] == group1][self.data_column]
2237
- raw_data2 = self.raw_df[self.raw_df[self.grouping_column] == group2][self.data_column]
2238
-
2239
- if self.paired:
2240
- stat, p = stat_test(data1, data2, paired=True)
2241
- else:
2242
- stat, p = stat_test(data1, data2)
2230
+ stat, p = kruskal(*grouped_data)
2231
+ test_name = 'Kruskal-Wallis test'
2243
2232
 
2244
2233
  test_results.append({
2245
- 'Comparison': f'{group1} vs {group2}',
2234
+ 'Comparison': f'{unique_groups[0]} vs {unique_groups[1]} ({column})',
2246
2235
  'Test Statistic': stat,
2247
2236
  'p-value': p,
2248
2237
  'Test Name': test_name,
2249
- 'n_object': len(raw_data1) + len(raw_data2), # Raw sample size (objects/cells)
2250
- 'n_well': len(data1) + len(data2) if self.representation == 'well' else np.nan # Summarized size (wells)
2251
- })
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
+
2252
2243
  return test_results
2253
2244
 
2254
2245
  def perform_posthoc_tests(self, is_normal, unique_groups):
2255
2246
  """Perform post-hoc tests for multiple groups based on all_to_all flag."""
2256
2247
  if is_normal and len(unique_groups) > 2 and self.all_to_all:
2257
- # Tukey HSD Post-hoc when comparing all to all
2258
2248
  tukey_result = pairwise_tukeyhsd(self.df[self.data_column], self.df[self.grouping_column], alpha=0.05)
2259
2249
  posthoc_results = []
2260
2250
  for comparison, p_value in zip(tukey_result._results_table.data[1:], tukey_result.pvalues):
@@ -2267,12 +2257,10 @@ class spacrGraph:
2267
2257
  'p-value': p_value,
2268
2258
  'Test Name': 'Tukey HSD Post-hoc',
2269
2259
  'n_object': len(raw_data1) + len(raw_data2),
2270
- 'n_well': len(self.df[self.df[self.grouping_column] == comparison[0]]) + len(self.df[self.df[self.grouping_column] == comparison[1]])
2271
- })
2260
+ 'n_well': len(self.df[self.df[self.grouping_column] == comparison[0]]) + len(self.df[self.df[self.grouping_column] == comparison[1]])})
2272
2261
  return posthoc_results
2273
2262
 
2274
2263
  elif len(unique_groups) > 2 and not self.all_to_all and self.compare_group:
2275
- # Dunn's post-hoc test using Pingouin
2276
2264
  dunn_result = pg.pairwise_tests(data=self.df, dv=self.data_column, between=self.grouping_column, padjust='bonf', test='dunn')
2277
2265
  posthoc_results = []
2278
2266
  for idx, row in dunn_result.iterrows():
@@ -2283,138 +2271,379 @@ class spacrGraph:
2283
2271
  'p-value': row['p-val'],
2284
2272
  'Test Name': 'Dunn’s Post-hoc',
2285
2273
  'n_object': None,
2286
- 'n_well': None
2287
- })
2274
+ 'n_well': None})
2275
+
2288
2276
  return posthoc_results
2289
2277
  return []
2290
-
2278
+
2291
2279
  def create_plot(self, ax=None):
2292
2280
  """Create and display the plot based on the chosen graph type."""
2293
- # Optional: Remove outliers for plotting
2294
- if self.remove_outliers:
2295
- self.df = self.remove_outliers_from_plot()
2296
2281
 
2297
- # Perform normality tests
2298
- is_normal, normality_results = self.perform_normality_tests()
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'
2299
2375
 
2300
- # Perform Levene's test for equal variance
2301
- unique_groups = self.df[self.grouping_column].unique()
2302
- levene_stat, levene_p = self.perform_levene_test(unique_groups)
2303
- levene_result = {
2304
- 'Comparison': 'Levene’s test for equal variance',
2305
- 'Test Statistic': levene_stat,
2306
- 'p-value': levene_p,
2307
- 'Test Name': 'Levene’s Test'
2308
- }
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)]
2309
2379
 
2310
- # Perform statistical tests
2311
- stat_results = self.perform_statistical_tests(unique_groups, is_normal)
2380
+ # Stagger lines to avoid overlap
2381
+ line_y = y_max + (0.1 * y_max) * (idx + 1)
2312
2382
 
2313
- # Perform post-hoc tests if applicable
2314
- posthoc_results = self.perform_posthoc_tests(is_normal, unique_groups)
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')
2315
2385
 
2316
- # Combine all test results
2317
- self.results_df = pd.DataFrame(normality_results + [levene_result] + stat_results + posthoc_results)
2386
+ # Add the significance marker
2387
+ ax.text((x1 + x2) / 2, line_y, significance, ha='center', va='bottom', fontsize=12)
2318
2388
 
2319
- # Add sample size column
2320
- sample_sizes = self.df.groupby(self.grouping_column)[self.data_column].count().reset_index(name='n')
2321
- self.results_df['n'] = self.results_df['Comparison'].apply(
2322
- lambda x: next((sample_sizes[sample_sizes[self.grouping_column] == g]['n'].values[0] for g in sample_sizes[self.grouping_column] if g in x), np.nan)
2323
- )
2389
+ # Optional: Remove outliers for plotting
2390
+ if self.remove_outliers:
2391
+ self.df = self.remove_outliers_from_plot()
2324
2392
 
2325
- # Dynamically set figure dimensions based on the number of unique groups
2326
- num_groups = len(unique_groups)
2327
- bar_width = 0.6 # Set the desired thickness of each bar
2328
- spacing_between_groups = 0.3 # Set the desired spacing between bars and axis
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')
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)
2329
2400
 
2330
- fig_width = num_groups * (bar_width + spacing_between_groups) # Dynamically calculate the figure width
2331
- fig_height = 6 # Fixed height for the plot
2401
+ num_groups = len(self.data_column)*len(self.grouping_column)
2402
+ num_data_columns = len(self.data_column)
2403
+ self.bar_width = 0.4
2404
+ spacing_between_groups = self.bar_width/0.5
2332
2405
 
2406
+ self.fig_width = (num_groups * self.bar_width) + (spacing_between_groups * num_groups)
2407
+ self.fig_height = self.fig_width/2
2408
+
2333
2409
  if ax is None:
2334
- self.fig, ax = plt.subplots(figsize=(fig_width, fig_height)) # Store the figure in self.fig
2410
+ self.fig, ax = plt.subplots(figsize=(self.fig_height, self.fig_width))
2335
2411
  else:
2336
- self.fig = ax.figure # Store the figure if ax is provided
2337
-
2338
- sns.set(style="ticks")
2339
- color_palette = self.sns_palette if not self.colors else self.colors
2412
+ self.fig = ax.figure
2340
2413
 
2341
- # Calculate x-axis limits to ensure equal space between the bars and the y-axis
2342
- xlim_lower = -0.5 # Ensures space between the y-axis and the first category
2343
- xlim_upper = num_groups - 0.5 # Ensures space after the last category
2344
- ax.set_xlim(xlim_lower, xlim_upper)
2345
-
2346
- if self.summary_func is None:
2347
- sns.stripplot(x=self.grouping_column, y=self.data_column, data=self.df, palette=color_palette, jitter=True, alpha=0.6, ax=ax)
2348
- elif self.graph_type == 'bar':
2349
- self._create_bar_plot(bar_width, ax)
2414
+ if len(self.data_column) == 1:
2415
+ self.hue=self.grouping_column
2416
+ self.jitter_bar_dodge = False
2417
+ else:
2418
+ self.hue='Data Column'
2419
+ self.jitter_bar_dodge = True
2420
+
2421
+ # Handle the different plot types based on `graph_type`
2422
+ if self.graph_type == 'bar':
2423
+ self._create_bar_plot(ax)
2424
+ elif self.graph_type == 'jitter':
2425
+ self._create_jitter_plot(ax)
2350
2426
  elif self.graph_type == 'box':
2351
2427
  self._create_box_plot(ax)
2352
2428
  elif self.graph_type == 'violin':
2353
2429
  self._create_violin_plot(ax)
2354
- elif self.graph_type == 'jitter':
2355
- self._create_jitter_plot(ax)
2356
2430
  else:
2357
- raise ValueError(f"Invalid graph_type: {self.graph_type}. Choose from 'bar', 'box', 'violin', or 'jitter'.")
2358
-
2431
+ raise ValueError(f"Unknown graph type: {self.graph_type}")
2432
+
2359
2433
  # Set y-axis start
2360
- if self.y_axis_start is not None:
2361
- ax.set_ylim(bottom=self.y_axis_start)
2362
-
2363
- # Add ticks, remove grid, and save plot
2364
- ax.minorticks_on()
2365
- ax.tick_params(axis='x', which='minor', bottom=False) # Disable minor ticks on x-axis
2366
- ax.tick_params(axis='x', which='major', length=6, width=2, direction='out')
2367
- ax.tick_params(axis='y', which='major', length=6, width=2, direction='out')
2368
- ax.tick_params(axis='y', which='minor', length=4, width=1, direction='out')
2369
- sns.despine(ax=ax, top=True, right=True)
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)
2370
2439
 
2440
+ sns.despine(ax=ax, top=True, right=True)
2441
+ ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), title='Data Column') # Move the legend outside the plot
2442
+ ax.set_xlabel('')
2443
+ x_positions = _get_positions(self, ax)
2444
+
2445
+ if len(self.data_column) == 1:
2446
+ ax.legend().remove()
2447
+ ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right')
2448
+
2449
+ elif len(self.data_column) > 1:
2450
+ ax.set_xticks([])
2451
+ ax.tick_params(bottom=False)
2452
+ ax.set_xticklabels([])
2453
+ legend_ax = self.fig.add_axes([0.1, -0.2, 0.62, 0.2]) # Position the table closer to the graph
2454
+ legend_ax.set_axis_off()
2455
+
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
+
2371
2461
  if self.save:
2372
2462
  self._save_results()
2373
2463
 
2374
- plt.show() # Ensure the plot is shown, but plt.show() doesn't clear the figure context
2375
-
2376
- def get_figure(self):
2377
- """Return the generated figure."""
2378
- return self.fig
2464
+ ax.margins(x=0.12)
2379
2465
 
2380
- def _create_bar_plot(self, bar_width, ax):
2466
+ def _create_bar_plot(self, ax):
2381
2467
  """Helper method to create a bar plot with consistent bar thickness and centered error bars."""
2382
- summary_df = self.df.groupby(self.grouping_column)[self.data_column].agg([self.summary_func, 'std', 'sem'])
2383
-
2384
- if self.error_bar_type == 'std':
2385
- error_bars = summary_df['std']
2386
- elif self.error_bar_type == 'sem':
2387
- error_bars = summary_df['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')
2388
2474
  else:
2389
- raise ValueError(f"Invalid error_bar_type: {self.error_bar_type}. Choose either 'std' or 'sem'.")
2390
-
2391
- sns.barplot(x=self.grouping_column, y=self.summary_func, data=summary_df.reset_index(), ci=None, palette=self.sns_palette, width=bar_width, ax=ax)
2392
-
2393
- # Plot the error bars
2394
- ax.errorbar(x=np.arange(len(summary_df)), y=summary_df[self.summary_func], yerr=error_bars, fmt='none', c='black', capsize=5)
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]
2497
+ ax.errorbar(x=x_bar, y=bar.get_height(), yerr=err, fmt='none', c='black', capsize=5, lw=2)
2498
+
2499
+ # Set legend and labels
2500
+ ax.set_xlabel(self.grouping_column)
2395
2501
 
2396
2502
  def _create_jitter_plot(self, ax):
2397
- """Helper method to create a jitter plot (strip plot)."""
2398
- sns.stripplot(x=self.grouping_column, y=self.data_column, data=self.df, palette=self.sns_palette, jitter=True, alpha=0.6, ax=ax)
2399
-
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
+
2400
2526
  def _create_box_plot(self, ax):
2401
- """Helper method to create a box plot."""
2402
- sns.boxplot(x=self.grouping_column, y=self.data_column, data=self.df, 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)
2403
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
+
2404
2550
  def _create_violin_plot(self, ax):
2405
- """Helper method to create a violin plot."""
2406
- sns.violinplot(x=self.grouping_column, y=self.data_column, data=self.df, 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')
2407
2574
 
2408
2575
  def _save_results(self):
2409
2576
  """Helper method to save the plot and results."""
2410
2577
  os.makedirs(self.output_dir, exist_ok=True)
2411
- plot_path = os.path.join(self.output_dir, 'grouped_plot.png')
2412
- self.fig.savefig(plot_path)
2413
- 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")
2414
2581
  self.results_df.to_csv(results_path, index=False)
2415
2582
  print(f"Plot saved to {plot_path}")
2416
2583
  print(f"Test results saved to {results_path}")
2417
2584
 
2418
2585
  def get_results(self):
2419
2586
  """Return the results dataframe."""
2420
- return self.results_df
2587
+ return self.results_df
2588
+
2589
+ def get_figure(self):
2590
+ """Return the generated figure."""
2591
+ return self.fig
2592
+
2593
+ def plot_data_from_db(settings):
2594
+ from .io import _read_db
2595
+ from .utils import annotate_conditions
2596
+ """
2597
+ Extracts the specified table from the SQLite database and plots a specified column.
2598
+
2599
+ Args:
2600
+ db_path (str): The path to the SQLite database.
2601
+ table_name (str): The name of the table to extract.
2602
+ column_name (str): The column to plot from the table.
2603
+
2604
+ Returns:
2605
+ df (pd.DataFrame): The extracted table as a DataFrame.
2606
+ """
2607
+
2608
+ db_loc = os.path.join(settings['src'], 'measurements',settings['database'])
2609
+
2610
+ [df] = _read_db(db_loc, tables=[settings['table_name']])
2611
+
2612
+ df = annotate_conditions(df,
2613
+ cells=settings['cell_types'],
2614
+ cell_loc=settings['cell_plate_metadata'],
2615
+ pathogens=settings['pathogen_types'],
2616
+ pathogen_loc=settings['pathogen_plate_metadata'],
2617
+ treatments=settings['treatments'],
2618
+ treatment_loc=settings['treatment_plate_metadata'])
2619
+
2620
+ df['prc'] = df['plate'].astype(str) + '_' + df['row'].astype(str) + '_' + df['col'].astype(str)
2621
+ df = df.dropna(subset=settings['column_name'])
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))
2623
+
2624
+ spacr_graph = spacrGraph(
2625
+ df=df, # Your DataFrame
2626
+ grouping_column=settings['grouping_column'], # Column for grouping the data (x-axis)
2627
+ data_column=settings['column_name'], # Column for the data (y-axis)
2628
+ graph_type=settings['graph_type'], # Type of plot ('bar', 'box', 'violin', 'jitter')
2629
+ summary_func='mean', # Function to summarize data (e.g., 'mean', 'median')
2630
+ colors=None, # Custom colors for the plot (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)
2634
+ error_bar_type='std', # Type of error bar ('std' or 'sem')
2635
+ representation='well',
2636
+ theme=settings['theme'], # Seaborn color palette theme (e.g., 'pastel', 'muted')
2637
+ )
2638
+
2639
+ # Create the plot
2640
+ spacr_graph.create_plot()
2641
+
2642
+ # Get the figure object if needed
2643
+ fig = spacr_graph.get_figure()
2644
+ plt.show()
2645
+
2646
+ # Optional: Get the results DataFrame containing statistical test results
2647
+ results_df = spacr_graph.get_results()
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.32
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=Lv-QFD_NwP9pdsUIiJ--XHJN-jQBkFz_AI9y4i36jEA,105506
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.32.dist-info/LICENSE,sha256=SR-2MeGc6SCM1UORJYyarSWY_A-JaOMFDj7ReSs9tRM,1083
154
- spacr-0.3.32.dist-info/METADATA,sha256=zmqGrPPpr-pTpLbwPpCl0zwLA2oaZ1kGNo90t-nUF5g,5949
155
- spacr-0.3.32.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
156
- spacr-0.3.32.dist-info/entry_points.txt,sha256=BMC0ql9aNNpv8lUZ8sgDLQMsqaVnX5L535gEhKUP5ho,296
157
- spacr-0.3.32.dist-info/top_level.txt,sha256=GJPU8FgwRXGzKeut6JopsSRY2R8T3i9lDgya42tLInY,6
158
- spacr-0.3.32.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