geometallurgy 0.4.13__py3-none-any.whl → 0.4.15__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.
Files changed (48) hide show
  1. elphick/geomet/__init__.py +11 -11
  2. elphick/geomet/base.py +1133 -1133
  3. elphick/geomet/block_model.py +319 -319
  4. elphick/geomet/config/__init__.py +1 -1
  5. elphick/geomet/config/config_read.py +39 -39
  6. elphick/geomet/config/flowsheet_example_partition.yaml +31 -31
  7. elphick/geomet/config/flowsheet_example_simple.yaml +25 -25
  8. elphick/geomet/config/mc_config.yml +35 -35
  9. elphick/geomet/data/downloader.py +39 -39
  10. elphick/geomet/data/register.csv +12 -12
  11. elphick/geomet/datasets/__init__.py +2 -2
  12. elphick/geomet/datasets/datasets.py +47 -47
  13. elphick/geomet/datasets/downloader.py +40 -40
  14. elphick/geomet/datasets/register.csv +12 -12
  15. elphick/geomet/datasets/sample_data.py +196 -196
  16. elphick/geomet/extras.py +35 -35
  17. elphick/geomet/flowsheet/__init__.py +1 -1
  18. elphick/geomet/flowsheet/flowsheet.py +1216 -1216
  19. elphick/geomet/flowsheet/loader.py +99 -99
  20. elphick/geomet/flowsheet/operation.py +256 -256
  21. elphick/geomet/flowsheet/stream.py +39 -39
  22. elphick/geomet/interval_sample.py +641 -641
  23. elphick/geomet/io.py +379 -379
  24. elphick/geomet/plot.py +147 -147
  25. elphick/geomet/sample.py +28 -28
  26. elphick/geomet/utils/amenability.py +49 -49
  27. elphick/geomet/utils/block_model_converter.py +93 -93
  28. elphick/geomet/utils/components.py +136 -136
  29. elphick/geomet/utils/data.py +49 -49
  30. elphick/geomet/utils/estimates.py +108 -108
  31. elphick/geomet/utils/interp.py +193 -193
  32. elphick/geomet/utils/interp2.py +134 -134
  33. elphick/geomet/utils/layout.py +72 -72
  34. elphick/geomet/utils/moisture.py +61 -61
  35. elphick/geomet/utils/pandas.py +378 -378
  36. elphick/geomet/utils/parallel.py +29 -29
  37. elphick/geomet/utils/partition.py +63 -63
  38. elphick/geomet/utils/size.py +51 -51
  39. elphick/geomet/utils/timer.py +80 -80
  40. elphick/geomet/utils/viz.py +56 -56
  41. elphick/geomet/validate.py.hide +176 -176
  42. {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/LICENSE +21 -21
  43. {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/METADATA +2 -3
  44. geometallurgy-0.4.15.dist-info/RECORD +48 -0
  45. {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/WHEEL +1 -1
  46. elphick/geomet/utils/output.html +0 -617
  47. geometallurgy-0.4.13.dist-info/RECORD +0 -49
  48. {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/entry_points.txt +0 -0
@@ -1,29 +1,29 @@
1
- from joblib import Parallel
2
- from tqdm import tqdm
3
-
4
-
5
- class TqdmParallel(Parallel):
6
- def __init__(self, *args, **kwargs):
7
- self._desc = kwargs.pop('desc', None) # Get the description from kwargs
8
- self._tqdm = tqdm(total=kwargs.pop('total', None), desc=self._desc) # Pass the description to tqdm
9
- super().__init__(*args, **kwargs)
10
-
11
- def __call__(self, iterable):
12
- iterable = list(iterable)
13
- self._tqdm.total = len(iterable)
14
- result = super().__call__(iterable)
15
- self._tqdm.close()
16
- return result
17
-
18
- def _print(self, msg, *msg_args):
19
- return
20
-
21
- def print_progress(self):
22
- self._tqdm.update()
23
-
24
- def _dispatch(self, batch):
25
- job_idx = super()._dispatch(batch)
26
- return job_idx
27
-
28
- def _collect(self, output):
29
- return super()._collect(output)
1
+ from joblib import Parallel
2
+ from tqdm import tqdm
3
+
4
+
5
+ class TqdmParallel(Parallel):
6
+ def __init__(self, *args, **kwargs):
7
+ self._desc = kwargs.pop('desc', None) # Get the description from kwargs
8
+ self._tqdm = tqdm(total=kwargs.pop('total', None), desc=self._desc) # Pass the description to tqdm
9
+ super().__init__(*args, **kwargs)
10
+
11
+ def __call__(self, iterable):
12
+ iterable = list(iterable)
13
+ self._tqdm.total = len(iterable)
14
+ result = super().__call__(iterable)
15
+ self._tqdm.close()
16
+ return result
17
+
18
+ def _print(self, msg, *msg_args):
19
+ return
20
+
21
+ def print_progress(self):
22
+ self._tqdm.update()
23
+
24
+ def _dispatch(self, batch):
25
+ job_idx = super()._dispatch(batch)
26
+ return job_idx
27
+
28
+ def _collect(self, output):
29
+ return super()._collect(output)
@@ -1,63 +1,63 @@
1
- import importlib
2
- from functools import partial
3
-
4
- import numpy as np
5
- import pandas as pd
6
-
7
-
8
- def perfect(x: np.ndarray, d50: float) -> np.ndarray:
9
- """A perfect partition
10
-
11
- Args:
12
- x: The input dimension, e.g. size or density
13
- d50: The cut-point
14
-
15
- Returns:
16
-
17
- """
18
- pn: np.ndarray = np.where(x >= d50, 1.0, 0.0)
19
- return pn
20
-
21
-
22
- def napier_munn(x: np.ndarray, d50: float, ep: float) -> np.ndarray:
23
- """The Napier-Munn partition (1998)
24
-
25
- REF: https://www.sciencedirect.com/science/article/pii/S1474667016453036
26
-
27
- Args:
28
- x: The input dimension, e.g. size or density
29
- d50: The cut-point
30
- ep: The Escarte Probable
31
-
32
- Returns:
33
-
34
- """
35
- pn: np.ndarray = 1 / (1 + np.exp(1.099 * (d50 - x) / ep))
36
- return pn
37
-
38
-
39
- def napier_munn_size(size: np.ndarray, d50: float, ep: float) -> np.ndarray:
40
- return napier_munn(size, d50, ep)
41
-
42
-
43
- def napier_munn_density(density: np.ndarray, d50: float, ep: float) -> np.ndarray:
44
- return napier_munn(density, d50, ep)
45
-
46
-
47
- napier_munn_size_1mm = partial(napier_munn_size, d50=1.0, ep=0.1)
48
-
49
-
50
- def load_partition_function(module_name, function_name):
51
- module = importlib.import_module(module_name)
52
- return getattr(module, function_name)
53
-
54
- # if __name__ == '__main__':
55
- # da = np.arange(0, 10)
56
- # PN = perfect(da, d50=6.3)
57
- # df = pd.DataFrame([da, PN], index=['da', 'pn']).T
58
- # print(df)
59
- #
60
- # da = np.arange(0, 10)
61
- # PN = napier_munn(da, d50=6.3, ep=0.1)
62
- # df = pd.DataFrame([da, PN], index=['da', 'pn']).T
63
- # print(df)
1
+ import importlib
2
+ from functools import partial
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+
8
+ def perfect(x: np.ndarray, d50: float) -> np.ndarray:
9
+ """A perfect partition
10
+
11
+ Args:
12
+ x: The input dimension, e.g. size or density
13
+ d50: The cut-point
14
+
15
+ Returns:
16
+
17
+ """
18
+ pn: np.ndarray = np.where(x >= d50, 1.0, 0.0)
19
+ return pn
20
+
21
+
22
+ def napier_munn(x: np.ndarray, d50: float, ep: float) -> np.ndarray:
23
+ """The Napier-Munn partition (1998)
24
+
25
+ REF: https://www.sciencedirect.com/science/article/pii/S1474667016453036
26
+
27
+ Args:
28
+ x: The input dimension, e.g. size or density
29
+ d50: The cut-point
30
+ ep: The Escarte Probable
31
+
32
+ Returns:
33
+
34
+ """
35
+ pn: np.ndarray = 1 / (1 + np.exp(1.099 * (d50 - x) / ep))
36
+ return pn
37
+
38
+
39
+ def napier_munn_size(size: np.ndarray, d50: float, ep: float) -> np.ndarray:
40
+ return napier_munn(size, d50, ep)
41
+
42
+
43
+ def napier_munn_density(density: np.ndarray, d50: float, ep: float) -> np.ndarray:
44
+ return napier_munn(density, d50, ep)
45
+
46
+
47
+ napier_munn_size_1mm = partial(napier_munn_size, d50=1.0, ep=0.1)
48
+
49
+
50
+ def load_partition_function(module_name, function_name):
51
+ module = importlib.import_module(module_name)
52
+ return getattr(module, function_name)
53
+
54
+ # if __name__ == '__main__':
55
+ # da = np.arange(0, 10)
56
+ # PN = perfect(da, d50=6.3)
57
+ # df = pd.DataFrame([da, PN], index=['da', 'pn']).T
58
+ # print(df)
59
+ #
60
+ # da = np.arange(0, 10)
61
+ # PN = napier_munn(da, d50=6.3, ep=0.1)
62
+ # df = pd.DataFrame([da, PN], index=['da', 'pn']).T
63
+ # print(df)
@@ -1,51 +1,51 @@
1
- from typing import Union
2
-
3
- import numpy as np
4
- from pandas import IntervalIndex
5
- from pandas.arrays import IntervalArray
6
-
7
-
8
- def mean_size(size_intervals: Union[IntervalArray, IntervalIndex]) -> np.ndarray:
9
- """Geometric mean size
10
-
11
- Size calculations are performed using the geometric mean, not the arithmetic mean
12
-
13
- NOTE: If geometric mean is used for the pan fraction (0.0mm retained) it will return zero, which is an
14
- edge size not mean size. So the mean ratio of the geometric mean to the arithmetic mean for all other
15
- fractions is used for the bottom fraction.
16
-
17
-
18
- Args:
19
- size_intervals: A pandas IntervalArray or IntervalIndex
20
-
21
- Returns:
22
-
23
- """
24
-
25
- intervals = size_intervals.copy()
26
- res = np.array((intervals.left * intervals.right) ** 0.5)
27
-
28
- geomean_mean_ratio: float = float(np.mean((res[0:-1] / intervals.mid[0:-1])))
29
-
30
- if np.isclose(size_intervals.min().left, 0.0):
31
- res[np.isclose(size_intervals.left, 0.0)] = size_intervals.min().mid * geomean_mean_ratio
32
-
33
- return res
34
-
35
-
36
- # REF: https://www.globalgilson.com/blog/sieve-sizes
37
-
38
- sizes_iso_565 = [63.0, 56.0, 53.0, 50.0, 45.0, 40.0, 37.5, 35.5, 31.5, 28.0, 26.5, 25.0, 22.4, 20.0,
39
- 19.0, 18.0, 16.0, 14.0, 13.2, 12.5, 11.2, 10.0, 9.5, 9.0, 8.0, 7.1, 6.7, 6.3, 5.6,
40
- 5.0, 4.75, 4.5, 4.0, 3.55, 3.35, 3.15, 2.8, 2.5, 2.36, 2.0, 1.8, 1.7, 1.6, 1.4, 1.25,
41
- 1.18, 1.12, 1.0, 0.900, 0.850, 0.800, 0.710, 0.630, 0.600, 0.560, 0.500, 0.450, 0.425,
42
- 0.400, 0.355, 0.315, 0.300, 0.280, 0.250, 0.224, 0.212, 0.200, 0.180, 0.160, 0.150, 0.140,
43
- 0.125, 0.112, 0.106, 0.100, 0.090, 0.080, 0.075, 0.071, 0.063, 0.056, 0.053, 0.050, 0.045,
44
- 0.040, 0.038, 0.036, 0.032, 0.025, 0.020, 0.0]
45
-
46
- sizes_astm_e11 = [100.0, 90.0, 75.0, 63.0, 53.0, 50.0, 45.0, 37.5, 31.5, 26.5, 25.0, 22.4, 19.0, 16.0,
47
- 13.2, 12.5, 11.2, 9.5, 8.0, 6.7, 6.3, 5.6, 4.75, 4.0, 3.35, 2.8, 2.36, 2.0, 1.7, 1.4,
48
- 1.18, 1.0, 0.850, 0.710, 0.600, 0.500, 0.425, 0.355, 0.300, 0.250, 0.212, 0.180, 0.150,
49
- 0.125, 0.106, 0.090, 0.075, 0.063, 0.053, 0.045, 0.038, 0.032, 0.025, 0.020, 0.0]
50
-
51
- sizes_all = sorted(list(set(sizes_astm_e11).union(set(sizes_iso_565))), reverse=True)
1
+ from typing import Union
2
+
3
+ import numpy as np
4
+ from pandas import IntervalIndex
5
+ from pandas.arrays import IntervalArray
6
+
7
+
8
+ def mean_size(size_intervals: Union[IntervalArray, IntervalIndex]) -> np.ndarray:
9
+ """Geometric mean size
10
+
11
+ Size calculations are performed using the geometric mean, not the arithmetic mean
12
+
13
+ NOTE: If geometric mean is used for the pan fraction (0.0mm retained) it will return zero, which is an
14
+ edge size not mean size. So the mean ratio of the geometric mean to the arithmetic mean for all other
15
+ fractions is used for the bottom fraction.
16
+
17
+
18
+ Args:
19
+ size_intervals: A pandas IntervalArray or IntervalIndex
20
+
21
+ Returns:
22
+
23
+ """
24
+
25
+ intervals = size_intervals.copy()
26
+ res = np.array((intervals.left * intervals.right) ** 0.5)
27
+
28
+ geomean_mean_ratio: float = float(np.mean((res[0:-1] / intervals.mid[0:-1])))
29
+
30
+ if np.isclose(size_intervals.min().left, 0.0):
31
+ res[np.isclose(size_intervals.left, 0.0)] = size_intervals.min().mid * geomean_mean_ratio
32
+
33
+ return res
34
+
35
+
36
+ # REF: https://www.globalgilson.com/blog/sieve-sizes
37
+
38
+ sizes_iso_565 = [63.0, 56.0, 53.0, 50.0, 45.0, 40.0, 37.5, 35.5, 31.5, 28.0, 26.5, 25.0, 22.4, 20.0,
39
+ 19.0, 18.0, 16.0, 14.0, 13.2, 12.5, 11.2, 10.0, 9.5, 9.0, 8.0, 7.1, 6.7, 6.3, 5.6,
40
+ 5.0, 4.75, 4.5, 4.0, 3.55, 3.35, 3.15, 2.8, 2.5, 2.36, 2.0, 1.8, 1.7, 1.6, 1.4, 1.25,
41
+ 1.18, 1.12, 1.0, 0.900, 0.850, 0.800, 0.710, 0.630, 0.600, 0.560, 0.500, 0.450, 0.425,
42
+ 0.400, 0.355, 0.315, 0.300, 0.280, 0.250, 0.224, 0.212, 0.200, 0.180, 0.160, 0.150, 0.140,
43
+ 0.125, 0.112, 0.106, 0.100, 0.090, 0.080, 0.075, 0.071, 0.063, 0.056, 0.053, 0.050, 0.045,
44
+ 0.040, 0.038, 0.036, 0.032, 0.025, 0.020, 0.0]
45
+
46
+ sizes_astm_e11 = [100.0, 90.0, 75.0, 63.0, 53.0, 50.0, 45.0, 37.5, 31.5, 26.5, 25.0, 22.4, 19.0, 16.0,
47
+ 13.2, 12.5, 11.2, 9.5, 8.0, 6.7, 6.3, 5.6, 4.75, 4.0, 3.35, 2.8, 2.36, 2.0, 1.7, 1.4,
48
+ 1.18, 1.0, 0.850, 0.710, 0.600, 0.500, 0.425, 0.355, 0.300, 0.250, 0.212, 0.180, 0.150,
49
+ 0.125, 0.106, 0.090, 0.075, 0.063, 0.053, 0.045, 0.038, 0.032, 0.025, 0.020, 0.0]
50
+
51
+ sizes_all = sorted(list(set(sizes_astm_e11).union(set(sizes_iso_565))), reverse=True)
@@ -1,80 +1,80 @@
1
- """
2
- REF: https://ankitbko.github.io/blog/2021/04/logging-in-python/
3
- """
4
-
5
- import functools
6
- import logging
7
- from datetime import datetime
8
- from typing import Union
9
-
10
-
11
- class MyLogger:
12
- def __init__(self):
13
- logging.basicConfig(level=logging.DEBUG,
14
- format=' %(asctime)s - %(levelname)s - %(message)s')
15
-
16
- def get_logger(self, name=None):
17
- return logging.getLogger(name)
18
-
19
-
20
- def get_default_logger():
21
- return MyLogger().get_logger()
22
-
23
-
24
- def log_timer(_func=None, *, my_logger: Union[MyLogger, logging.Logger] = None):
25
- def decorator_log(func):
26
- @functools.wraps(func)
27
- def wrapper(*args, **kwargs):
28
- logger = get_default_logger()
29
- try:
30
- if my_logger is None:
31
- first_args = next(iter(args), None) # capture first arg to check for `self`
32
- logger_params = [ # does kwargs have any logger
33
- x
34
- for x in kwargs.values()
35
- if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
36
- ] + [ # # does args have any logger
37
- x
38
- for x in args
39
- if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
40
- ]
41
- if hasattr(first_args, "__dict__"): # is first argument `self`
42
- logger_params = logger_params + [
43
- x
44
- for x in first_args.__dict__.values() # does class (dict) members have any logger
45
- if isinstance(x, logging.Logger)
46
- or isinstance(x, MyLogger)
47
- ]
48
- h_logger = next(iter(logger_params), MyLogger()) # get the next/first/default logger
49
- else:
50
- h_logger = my_logger # logger is passed explicitly to the decorator
51
-
52
- if isinstance(h_logger, MyLogger):
53
- logger = h_logger.get_logger(func.__name__)
54
- else:
55
- logger = h_logger
56
-
57
- # args_repr = [repr(a) for a in args]
58
- # kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
59
- # signature = ", ".join(args_repr + kwargs_repr)
60
- # logger.debug(f"function {func.__name__} called with args {signature}")
61
-
62
- except Exception:
63
- pass
64
-
65
- try:
66
- _tic = datetime.now()
67
- result = func(*args, **kwargs)
68
- _toc = datetime.now()
69
- logger.info(f"Elapsed time for {func.__name__}: {_toc - _tic}")
70
- return result
71
- except Exception as e:
72
- logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
73
- raise e
74
-
75
- return wrapper
76
-
77
- if _func is None:
78
- return decorator_log
79
- else:
80
- return decorator_log(_func)
1
+ """
2
+ REF: https://ankitbko.github.io/blog/2021/04/logging-in-python/
3
+ """
4
+
5
+ import functools
6
+ import logging
7
+ from datetime import datetime
8
+ from typing import Union
9
+
10
+
11
+ class MyLogger:
12
+ def __init__(self):
13
+ logging.basicConfig(level=logging.DEBUG,
14
+ format=' %(asctime)s - %(levelname)s - %(message)s')
15
+
16
+ def get_logger(self, name=None):
17
+ return logging.getLogger(name)
18
+
19
+
20
+ def get_default_logger():
21
+ return MyLogger().get_logger()
22
+
23
+
24
+ def log_timer(_func=None, *, my_logger: Union[MyLogger, logging.Logger] = None):
25
+ def decorator_log(func):
26
+ @functools.wraps(func)
27
+ def wrapper(*args, **kwargs):
28
+ logger = get_default_logger()
29
+ try:
30
+ if my_logger is None:
31
+ first_args = next(iter(args), None) # capture first arg to check for `self`
32
+ logger_params = [ # does kwargs have any logger
33
+ x
34
+ for x in kwargs.values()
35
+ if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
36
+ ] + [ # # does args have any logger
37
+ x
38
+ for x in args
39
+ if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
40
+ ]
41
+ if hasattr(first_args, "__dict__"): # is first argument `self`
42
+ logger_params = logger_params + [
43
+ x
44
+ for x in first_args.__dict__.values() # does class (dict) members have any logger
45
+ if isinstance(x, logging.Logger)
46
+ or isinstance(x, MyLogger)
47
+ ]
48
+ h_logger = next(iter(logger_params), MyLogger()) # get the next/first/default logger
49
+ else:
50
+ h_logger = my_logger # logger is passed explicitly to the decorator
51
+
52
+ if isinstance(h_logger, MyLogger):
53
+ logger = h_logger.get_logger(func.__name__)
54
+ else:
55
+ logger = h_logger
56
+
57
+ # args_repr = [repr(a) for a in args]
58
+ # kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
59
+ # signature = ", ".join(args_repr + kwargs_repr)
60
+ # logger.debug(f"function {func.__name__} called with args {signature}")
61
+
62
+ except Exception:
63
+ pass
64
+
65
+ try:
66
+ _tic = datetime.now()
67
+ result = func(*args, **kwargs)
68
+ _toc = datetime.now()
69
+ logger.info(f"Elapsed time for {func.__name__}: {_toc - _tic}")
70
+ return result
71
+ except Exception as e:
72
+ logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
73
+ raise e
74
+
75
+ return wrapper
76
+
77
+ if _func is None:
78
+ return decorator_log
79
+ else:
80
+ return decorator_log(_func)
@@ -1,56 +1,56 @@
1
- from typing import Optional
2
-
3
- import pandas as pd
4
-
5
- import plotly.graph_objects as go
6
-
7
-
8
- def plot_parallel(data: pd.DataFrame, color: Optional[str] = None, title: Optional[str] = None) -> go.Figure:
9
- """Create an interactive parallel plot
10
-
11
- Useful to explore multi-dimensional data like mass-composition data
12
-
13
- Args:
14
- data: Dataframe to plot
15
- color: Optional color variable
16
- title: Optional plot title
17
-
18
- Returns:
19
-
20
- """
21
-
22
- # Kudos: https://stackoverflow.com/questions/72125802/parallel-coordinate-plot-in-plotly-with-continuous-
23
- # and-categorical-data
24
-
25
- categorical_columns = data.select_dtypes(include=['category', 'object'])
26
- col_list = []
27
-
28
- for col in data.columns:
29
- if col in categorical_columns: # categorical columns
30
- values = data[col].unique()
31
- value2dummy = dict(zip(values, range(
32
- len(values)))) # works if values are strings, otherwise we probably need to convert them
33
- data[col] = [value2dummy[v] for v in data[col]]
34
- col_dict = dict(
35
- label=col,
36
- tickvals=list(value2dummy.values()),
37
- ticktext=list(value2dummy.keys()),
38
- values=data[col],
39
- )
40
- else: # continuous columns
41
- col_dict = dict(
42
- range=(data[col].min(), data[col].max()),
43
- label=col,
44
- values=data[col],
45
- )
46
- col_list.append(col_dict)
47
-
48
- if color is None:
49
- fig = go.Figure(data=go.Parcoords(dimensions=col_list))
50
- else:
51
- fig = go.Figure(data=go.Parcoords(dimensions=col_list, line=dict(color=data[color])))
52
-
53
- fig.update_layout(title=title)
54
-
55
- return fig
56
-
1
+ from typing import Optional
2
+
3
+ import pandas as pd
4
+
5
+ import plotly.graph_objects as go
6
+
7
+
8
+ def plot_parallel(data: pd.DataFrame, color: Optional[str] = None, title: Optional[str] = None) -> go.Figure:
9
+ """Create an interactive parallel plot
10
+
11
+ Useful to explore multi-dimensional data like mass-composition data
12
+
13
+ Args:
14
+ data: Dataframe to plot
15
+ color: Optional color variable
16
+ title: Optional plot title
17
+
18
+ Returns:
19
+
20
+ """
21
+
22
+ # Kudos: https://stackoverflow.com/questions/72125802/parallel-coordinate-plot-in-plotly-with-continuous-
23
+ # and-categorical-data
24
+
25
+ categorical_columns = data.select_dtypes(include=['category', 'object'])
26
+ col_list = []
27
+
28
+ for col in data.columns:
29
+ if col in categorical_columns: # categorical columns
30
+ values = data[col].unique()
31
+ value2dummy = dict(zip(values, range(
32
+ len(values)))) # works if values are strings, otherwise we probably need to convert them
33
+ data[col] = [value2dummy[v] for v in data[col]]
34
+ col_dict = dict(
35
+ label=col,
36
+ tickvals=list(value2dummy.values()),
37
+ ticktext=list(value2dummy.keys()),
38
+ values=data[col],
39
+ )
40
+ else: # continuous columns
41
+ col_dict = dict(
42
+ range=(data[col].min(), data[col].max()),
43
+ label=col,
44
+ values=data[col],
45
+ )
46
+ col_list.append(col_dict)
47
+
48
+ if color is None:
49
+ fig = go.Figure(data=go.Parcoords(dimensions=col_list))
50
+ else:
51
+ fig = go.Figure(data=go.Parcoords(dimensions=col_list, line=dict(color=data[color])))
52
+
53
+ fig.update_layout(title=title)
54
+
55
+ return fig
56
+