well-log-toolkit 0.1.114__tar.gz → 0.1.116__tar.gz

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.
Files changed (20) hide show
  1. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/PKG-INFO +1 -1
  2. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/pyproject.toml +1 -1
  3. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/manager.py +19 -0
  4. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/visualization.py +128 -48
  5. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit.egg-info/PKG-INFO +1 -1
  6. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/README.md +0 -0
  7. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/setup.cfg +0 -0
  8. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/__init__.py +0 -0
  9. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/exceptions.py +0 -0
  10. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/las_file.py +0 -0
  11. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/operations.py +0 -0
  12. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/property.py +0 -0
  13. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/regression.py +0 -0
  14. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/statistics.py +0 -0
  15. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/utils.py +0 -0
  16. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit/well.py +0 -0
  17. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
  18. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
  19. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit.egg-info/requires.txt +0 -0
  20. {well_log_toolkit-0.1.114 → well_log_toolkit-0.1.116}/well_log_toolkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.114
3
+ Version: 0.1.116
4
4
  Summary: Fast LAS file processing with lazy loading and filtering for well log analysis
5
5
  Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "well-log-toolkit"
7
- version = "0.1.114"
7
+ version = "0.1.116"
8
8
  description = "Fast LAS file processing with lazy loading and filtering for well log analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -2466,6 +2466,9 @@ class WellDataManager:
2466
2466
  depth_range: Optional[tuple[float, float]] = None,
2467
2467
  show_colorbar: bool = True,
2468
2468
  show_legend: bool = True,
2469
+ regression: Optional[Union[str, dict]] = None,
2470
+ regression_by_color: Optional[Union[str, dict]] = None,
2471
+ regression_by_group: Optional[Union[str, dict]] = None,
2469
2472
  ) -> 'Crossplot':
2470
2473
  """
2471
2474
  Create a multi-well crossplot.
@@ -2531,6 +2534,19 @@ class WellDataManager:
2531
2534
  Show colorbar when using color mapping. Default: True
2532
2535
  show_legend : bool, optional
2533
2536
  Show legend. Default: True
2537
+ regression : str or dict, optional
2538
+ Regression type to apply to all data points. Can be a string (e.g., "linear") or
2539
+ dict with keys: type, line_color, line_width, line_style, line_alpha, x_range.
2540
+ Default: None
2541
+ regression_by_color : str or dict, optional
2542
+ Regression type to apply separately for each color group in the plot. Creates
2543
+ separate regression lines based on what determines colors in the visualization:
2544
+ explicit color mapping if specified, otherwise shape groups (e.g., wells when
2545
+ shape='well'). Accepts string or dict format. Default: None
2546
+ regression_by_group : str or dict, optional
2547
+ Regression type to apply separately for each well. Creates separate
2548
+ regression lines for each well. Accepts string or dict format.
2549
+ Default: None
2534
2550
 
2535
2551
  Returns
2536
2552
  -------
@@ -2608,6 +2624,9 @@ class WellDataManager:
2608
2624
  depth_range=depth_range,
2609
2625
  show_colorbar=show_colorbar,
2610
2626
  show_legend=show_legend,
2627
+ regression=regression,
2628
+ regression_by_color=regression_by_color,
2629
+ regression_by_group=regression_by_group,
2611
2630
  )
2612
2631
 
2613
2632
  def __repr__(self) -> str:
@@ -2827,9 +2827,10 @@ class Crossplot:
2827
2827
  dict with keys: type, line_color, line_width, line_style, line_alpha, x_range.
2828
2828
  Default: None
2829
2829
  regression_by_color : str or dict, optional
2830
- Regression type to apply separately for each color group. Creates separate
2831
- regression lines for each unique color value. Accepts string or dict format.
2832
- Default: None
2830
+ Regression type to apply separately for each color group in the plot. Creates
2831
+ separate regression lines based on what determines colors in the visualization:
2832
+ explicit color mapping if specified, otherwise shape groups (e.g., wells when
2833
+ shape='well'). Accepts string or dict format. Default: None
2833
2834
  regression_by_group : str or dict, optional
2834
2835
  Regression type to apply separately for each group (well or shape). Creates
2835
2836
  separate regression lines for each well or shape category. Accepts string or dict.
@@ -2961,6 +2962,9 @@ class Crossplot:
2961
2962
  self._regressions = {}
2962
2963
  self.regression_lines = {}
2963
2964
 
2965
+ # Pending regressions (added before plot() is called)
2966
+ self._pending_regressions = []
2967
+
2964
2968
  # Data cache
2965
2969
  self._data = None
2966
2970
 
@@ -3159,53 +3163,70 @@ class Crossplot:
3159
3163
  config = self._parse_regression_config(self.regression_by_color)
3160
3164
  reg_type = config['type']
3161
3165
 
3162
- if not self.color:
3163
- warnings.warn("regression_by_color specified but no color mapping defined, skipping")
3166
+ # Determine grouping column based on what's being used for colors in the plot
3167
+ group_column = None
3168
+ group_label = None
3169
+
3170
+ if self.color and 'color_val' in data.columns:
3171
+ # User specified explicit color mapping
3172
+ group_column = 'color_val'
3173
+ group_label = self.color
3174
+ elif self.shape == "well" and 'well' in data.columns:
3175
+ # When shape="well", each well gets a different color in the plot
3176
+ group_column = 'well'
3177
+ group_label = 'well'
3178
+ elif self.shape and self.shape != "well" and 'shape_val' in data.columns:
3179
+ # When shape is a property, each shape group gets a different color
3180
+ group_column = 'shape_val'
3181
+ group_label = self.shape
3182
+
3183
+ if group_column is None:
3184
+ warnings.warn(
3185
+ "regression_by_color specified but no color grouping detected in plot. "
3186
+ "Use color=<property>, shape='well', or shape=<property> parameter."
3187
+ )
3164
3188
  else:
3165
- # Get unique color groups
3166
- if 'color_val' in data.columns:
3167
- # For continuous color values, we need to bin them or use unique values
3168
- # Check if color is categorical (well, shape) or continuous
3169
- if self.color == 'depth' or pd.api.types.is_numeric_dtype(data['color_val']):
3170
- # For continuous values, we can't create separate regressions
3171
- warnings.warn(
3172
- f"regression_by_color requires categorical color mapping, "
3173
- f"but '{self.color}' is continuous. Use regression_by_group instead."
3189
+ # Check if color is categorical (not continuous like depth)
3190
+ if group_column == 'color_val' and (self.color == 'depth' or pd.api.types.is_numeric_dtype(data[group_column])):
3191
+ # For continuous values, we can't create separate regressions
3192
+ warnings.warn(
3193
+ f"regression_by_color requires categorical color mapping, "
3194
+ f"but '{self.color}' is continuous. Use regression_by_group instead."
3195
+ )
3196
+ else:
3197
+ # Categorical values - group and create regressions
3198
+ color_groups = data.groupby(group_column)
3199
+ n_groups = len(color_groups)
3200
+
3201
+ # Validate regression count
3202
+ if regression_count + n_groups > total_points / 2:
3203
+ raise ValueError(
3204
+ f"Too many regression lines requested: {regression_count + n_groups} lines "
3205
+ f"for {total_points} data points (average < 2 points per line). "
3206
+ f"Reduce the number of groups or use a different regression strategy."
3174
3207
  )
3175
- else:
3176
- # Categorical color values
3177
- color_groups = data.groupby('color_val')
3178
- n_groups = len(color_groups)
3179
-
3180
- # Validate regression count
3181
- if regression_count + n_groups > total_points / 2:
3182
- raise ValueError(
3183
- f"Too many regression lines requested: {regression_count + n_groups} lines "
3184
- f"for {total_points} data points (average < 2 points per line). "
3185
- f"Reduce the number of groups or use a different regression strategy."
3186
- )
3187
3208
 
3188
- for idx, (group_name, group_data) in enumerate(color_groups):
3189
- x_vals = group_data['x'].values
3190
- y_vals = group_data['y'].values
3191
- mask = np.isfinite(x_vals) & np.isfinite(y_vals)
3192
- if np.sum(mask) >= 2:
3193
- # Copy config and set default line color if not specified
3194
- group_config = config.copy()
3195
- if 'line_color' not in group_config:
3196
- group_config['line_color'] = regression_colors[color_idx % len(regression_colors)]
3197
-
3198
- # Skip legend update for all but last regression
3199
- is_last = (idx == n_groups - 1)
3200
- self._add_group_regression(
3201
- x_vals[mask], y_vals[mask],
3202
- reg_type,
3203
- name=f"{self.color}={group_name}",
3204
- config=group_config,
3205
- update_legend=is_last
3206
- )
3207
- regression_count += 1
3208
- color_idx += 1
3209
+ for idx, (group_name, group_data) in enumerate(color_groups):
3210
+ x_vals = group_data['x'].values
3211
+ y_vals = group_data['y'].values
3212
+ mask = np.isfinite(x_vals) & np.isfinite(y_vals)
3213
+ if np.sum(mask) >= 2:
3214
+ # Copy config and set default line color if not specified
3215
+ group_config = config.copy()
3216
+ if 'line_color' not in group_config:
3217
+ group_config['line_color'] = regression_colors[color_idx % len(regression_colors)]
3218
+
3219
+ # Skip legend update for all but last regression
3220
+ is_last = (idx == n_groups - 1)
3221
+ self._add_group_regression(
3222
+ x_vals[mask], y_vals[mask],
3223
+ reg_type,
3224
+ name=f"{group_label}={group_name}",
3225
+ config=group_config,
3226
+ update_legend=is_last
3227
+ )
3228
+ regression_count += 1
3229
+ color_idx += 1
3209
3230
 
3210
3231
  # Add regression by groups (well or shape)
3211
3232
  if self.regression_by_group:
@@ -3355,6 +3376,51 @@ class Crossplot:
3355
3376
  # Add automatic regressions if specified
3356
3377
  self._add_automatic_regressions(data)
3357
3378
 
3379
+ # Apply pending regressions (added via add_regression() before plot() was called)
3380
+ if self._pending_regressions:
3381
+ for pending in self._pending_regressions:
3382
+ # Get the already-fitted regression object
3383
+ reg_type = pending['regression_type']
3384
+ reg_name = pending['name'] if pending['name'] else reg_type
3385
+
3386
+ # Retrieve stored regression
3387
+ if reg_type in self._regressions and reg_name in self._regressions[reg_type]:
3388
+ reg = self._regressions[reg_type][reg_name]
3389
+
3390
+ # Draw the regression line
3391
+ try:
3392
+ x_line, y_line = reg.get_plot_data(x_range=pending['x_range'], num_points=200)
3393
+ except ValueError as e:
3394
+ warnings.warn(f"Could not generate plot data for {reg_type} regression: {e}")
3395
+ continue
3396
+
3397
+ # Create label
3398
+ label_parts = [reg_name]
3399
+ if pending['show_equation']:
3400
+ label_parts.append(reg.equation())
3401
+ if pending['show_r2']:
3402
+ label_parts.append(f"R² = {reg.r_squared:.4f}")
3403
+ label = "\n".join(label_parts)
3404
+
3405
+ # Plot line
3406
+ line = self.ax.plot(
3407
+ x_line, y_line,
3408
+ color=pending['line_color'],
3409
+ linewidth=pending['line_width'],
3410
+ linestyle=pending['line_style'],
3411
+ alpha=pending['line_alpha'],
3412
+ label=label
3413
+ )[0]
3414
+
3415
+ self.regression_lines[reg_name] = line
3416
+
3417
+ # Update legend once after all pending regressions
3418
+ if self.ax is not None:
3419
+ self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3420
+
3421
+ # Clear pending list
3422
+ self._pending_regressions = []
3423
+
3358
3424
  # Tight layout
3359
3425
  self.fig.tight_layout()
3360
3426
 
@@ -3575,7 +3641,7 @@ class Crossplot:
3575
3641
  reg_name = name if name else regression_type
3576
3642
  self._store_regression(regression_type, reg_name, reg)
3577
3643
 
3578
- # Plot regression line if figure exists
3644
+ # Plot regression line if figure exists, otherwise store for later
3579
3645
  if self.ax is not None:
3580
3646
  # Get plot data using the regression helper method
3581
3647
  try:
@@ -3606,6 +3672,20 @@ class Crossplot:
3606
3672
 
3607
3673
  # Update legend
3608
3674
  self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3675
+ else:
3676
+ # Store for later when plot() is called
3677
+ self._pending_regressions.append({
3678
+ 'regression_type': regression_type,
3679
+ 'name': name,
3680
+ 'line_color': line_color,
3681
+ 'line_width': line_width,
3682
+ 'line_style': line_style,
3683
+ 'line_alpha': line_alpha,
3684
+ 'show_equation': show_equation,
3685
+ 'show_r2': show_r2,
3686
+ 'x_range': x_range,
3687
+ 'kwargs': kwargs
3688
+ })
3609
3689
 
3610
3690
  return self
3611
3691
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.114
3
+ Version: 0.1.116
4
4
  Summary: Fast LAS file processing with lazy loading and filtering for well log analysis
5
5
  Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
6
6
  License: MIT