bs-python-utils 0.0.1__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.
@@ -0,0 +1,195 @@
1
+ """examples using my Altair functions"""
2
+
3
+ from pathlib import Path
4
+ from typing import Callable
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from vega_datasets import data
9
+
10
+ from bs_python_utils.bsutils import mkdir_if_needed
11
+ from bs_python_utils.bs_altair import (
12
+ alt_superposed_lineplot,
13
+ alt_superposed_faceted_lineplot,
14
+ alt_histogram_continuous,
15
+ alt_histogram_continuous,
16
+ alt_histogram_by,
17
+ alt_stacked_area,
18
+ alt_stacked_area_facets,
19
+ alt_scatterplot,
20
+ alt_linked_scatterplots,
21
+ alt_scatterplot_with_histo,
22
+ alt_density,
23
+ alt_faceted_densities,
24
+ alt_tick_plots,
25
+ alt_plot_fun,
26
+ plot_parameterized_estimates,
27
+ plot_true_sim2_facets,
28
+ plot_true_sim_facets,
29
+ )
30
+
31
+
32
+ FIG_DIRECTORY = "altair_figs"
33
+ save_path = mkdir_if_needed(Path.cwd() / FIG_DIRECTORY)
34
+
35
+ cars = data.cars()
36
+
37
+ ch = alt_superposed_lineplot(
38
+ cars,
39
+ "Horsepower",
40
+ "Weight_in_lbs",
41
+ "Origin",
42
+ save=save_path / "cars_superposed_lineplot",
43
+ )
44
+
45
+ ch = alt_superposed_faceted_lineplot(
46
+ cars,
47
+ "Horsepower",
48
+ "Weight_in_lbs",
49
+ "Origin",
50
+ "Year",
51
+ save=save_path / "cars_superposed_faceted_lineplot",
52
+ )
53
+
54
+ ch = alt_histogram_continuous(cars, "Horsepower", save=save_path / "cars_histo_cont")
55
+
56
+ ch = alt_histogram_by(
57
+ cars, "Origin", "Horsepower", str_agg="median", save=save_path / "cars_histo_by"
58
+ )
59
+
60
+ elec = data.iowa_electricity()
61
+
62
+ ch = alt_stacked_area(
63
+ elec,
64
+ "year",
65
+ "net_generation",
66
+ "source",
67
+ time_series=True,
68
+ title="Generators",
69
+ save=save_path / "elec_stacked_areas",
70
+ )
71
+
72
+ ch = alt_stacked_area_facets(
73
+ cars,
74
+ "Year",
75
+ "Displacement",
76
+ "Name",
77
+ "Origin",
78
+ time_series=True,
79
+ save=save_path / "cars_stacked_areas_facets",
80
+ )
81
+
82
+ ch = alt_scatterplot(
83
+ cars,
84
+ "Year",
85
+ "Displacement",
86
+ time_series=True,
87
+ title="Average car displacement",
88
+ aggreg="average",
89
+ save=save_path / "cars_scatter",
90
+ )
91
+
92
+ ch = alt_scatterplot(
93
+ cars,
94
+ "Year",
95
+ "Displacement",
96
+ time_series=True,
97
+ title="Average car displacement",
98
+ aggreg="average",
99
+ save=save_path / "cars_scatter_labx",
100
+ xlabel="Model year",
101
+ )
102
+
103
+ ch = alt_scatterplot(
104
+ cars,
105
+ "Horsepower",
106
+ "Displacement",
107
+ title="Car displacement",
108
+ color="Origin",
109
+ selection=True,
110
+ save=save_path / "cars_scatter_color",
111
+ xlabel="Horsepower",
112
+ )
113
+
114
+ ch = alt_linked_scatterplots(
115
+ cars,
116
+ "Horsepower",
117
+ "Displacement",
118
+ "Miles_per_Gallon",
119
+ "Origin",
120
+ save=save_path / "cars_linked_scatters",
121
+ )
122
+
123
+ ch = alt_scatterplot_with_histo(
124
+ cars,
125
+ "Horsepower",
126
+ "Displacement",
127
+ "Origin",
128
+ save=save_path / "cars_linked_scatter_histo",
129
+ )
130
+
131
+ ch = alt_density(cars, "Horsepower", save=save_path / "horsepower_density")
132
+
133
+ ch = alt_faceted_densities(
134
+ cars, "Horsepower", "Origin", save=save_path / "horsepower_distribs"
135
+ )
136
+
137
+
138
+ def fnp(x):
139
+ return x * x - 4.0
140
+
141
+
142
+ ch = alt_plot_fun(fnp, -2.0, 3.0, save=save_path / "plot_function")
143
+
144
+ # test plot_parameterized_estimates
145
+ nvals = 50
146
+ vals_p = np.arange(nvals) / (nvals - 1.0)
147
+ true_vals = np.column_stack((vals_p, np.ones(nvals)))
148
+ estimates_a = np.random.normal(size=((nvals, 2)), scale=0.2) + vals_p.reshape((-1, 1))
149
+ estimates_b = np.random.normal(size=((nvals, 2)), scale=0.2) + np.ones((nvals, 2))
150
+ estimates = np.zeros((nvals, 2, 2))
151
+ estimates[..., 0] = estimates_a
152
+ estimates[..., 1] = estimates_b
153
+
154
+ ch = plot_parameterized_estimates(
155
+ "Value of p",
156
+ vals_p,
157
+ ["a", "b"],
158
+ true_vals,
159
+ ["MLE", "MM"],
160
+ estimates,
161
+ colors=["black", "green", "blue"],
162
+ save=save_path / "ppe.html",
163
+ )
164
+
165
+ stats = np.reshape(estimates, (nvals, 4))
166
+ true_vals = stats + np.random.normal(loc=-0.1, scale=0.2, size=stats.shape)
167
+ ch = plot_true_sim_facets(
168
+ "Value of p",
169
+ vals_p,
170
+ ["a", "b", "c", "d"],
171
+ true_vals,
172
+ stats,
173
+ colors=["black", "red"],
174
+ ncols=2,
175
+ save=save_path / "ptsf.html",
176
+ )
177
+
178
+ stats2 = stats + np.random.normal(loc=0.1, scale=0.2, size=stats.shape)
179
+ ch = plot_true_sim2_facets(
180
+ "Value of p",
181
+ vals_p,
182
+ ["a", "b", "c", "d"],
183
+ true_vals,
184
+ stats,
185
+ stats2,
186
+ colors=["black", "red", "green"],
187
+ ncols=2,
188
+ save=save_path / "pts2f.html",
189
+ )
190
+
191
+ ch = alt_tick_plots(cars, "Weight_in_lbs", save=save_path / "weight_ticks")
192
+
193
+ ch = alt_tick_plots(
194
+ cars, ["Horsepower", "Weight_in_lbs"], save=save_path / "horse_weight_ticks"
195
+ )
@@ -0,0 +1,32 @@
1
+ """examples using distance_covariances"""
2
+
3
+ import numpy as np
4
+
5
+ from bs_python_utils.distance_covariances import pvalue_pdcov, dcov_dcor, pdcov_pdcor
6
+
7
+ # example page 2396 of Szekely and Rizzo 2014
8
+ n = 2000
9
+ do_bootstrap = False
10
+ Z1 = np.random.normal(size=n)
11
+ Z2 = np.random.normal(size=n)
12
+ Z3 = np.random.normal(size=n)
13
+ X = Z1 + Z3
14
+ Y = Z2 + Z3
15
+ Z = Z3
16
+ print("\n\n Test of page 2396")
17
+ dcov_XY = dcov_dcor(X, Y)
18
+ dcov_XZ = dcov_dcor(X, Z)
19
+ dcov_YZ = dcov_dcor(Y, Z)
20
+ pdcov_XYZ = pdcov_pdcor(X, Y, Z)
21
+ print(f" dCor(X, Y)={dcov_XY.dcor}, should be 0.2062 in large samples")
22
+ print(f" dCor(X, Z)={dcov_XZ.dcor}, should be 0.4319 in large samples")
23
+ print(f" dCor(Y, Z)={dcov_YZ.dcor}, should be 0.4319 in large samples")
24
+ print(f" pdCor(X, Y; Z)={pdcov_XYZ.pdcor}, should be 0.0242 in large samples")
25
+
26
+ if do_bootstrap:
27
+ ndraws = 499
28
+ pval = pvalue_pdcov(pdcov_XYZ)
29
+ print(
30
+ f"\n\n test stat={pdcov_XYZ.pdcov_stat: >.2f} has p-value {pval} for"
31
+ f" {ndraws} draws"
32
+ )
@@ -0,0 +1,25 @@
1
+ """ example of using bs_mem"""
2
+ import numpy as np
3
+ import tracemalloc
4
+
5
+ from bs_python_utils.bs_mem import (
6
+ memory_display_top,
7
+ memory_display_top_diffs,
8
+ memory_usage,
9
+ )
10
+
11
+ tracemalloc.start()
12
+ list_ex = list(range(10000))
13
+
14
+
15
+ v1 = np.ones(54546)
16
+ snapshot1 = tracemalloc.take_snapshot()
17
+ memory_display_top(snapshot1)
18
+ d = {i: li for (i, li) in enumerate(list_ex)}
19
+ m = np.random.normal(size=(3498, 12))
20
+ memory_usage(5)
21
+ snapshot2 = tracemalloc.take_snapshot()
22
+ memory_display_top_diffs(snapshot1, snapshot2)
23
+ tracemalloc.stop()
24
+ del d
25
+ memory_usage(5)
@@ -0,0 +1,37 @@
1
+ """examples using my Seaborn functions"""
2
+
3
+ import seaborn as sns
4
+
5
+ from bs_python_utils.bs_seaborn import (
6
+ bs_sns_bar_x_byf,
7
+ bs_sns_bar_x_byfg,
8
+ bs_sns_get_legend,
9
+ )
10
+
11
+ cars = sns.load_dataset("mpg")
12
+
13
+ g1 = bs_sns_bar_x_byf(
14
+ cars,
15
+ "horsepower",
16
+ "cylinders",
17
+ label_x="Horsepower",
18
+ label_f="Number of cylinders",
19
+ title="Mean HP by number of cylinders",
20
+ )
21
+
22
+ g2 = bs_sns_bar_x_byfg(
23
+ cars,
24
+ "horsepower",
25
+ "cylinders",
26
+ "origin",
27
+ label_x="Horsepower",
28
+ label_f="Number of cylinders",
29
+ label_g="Origin",
30
+ title="Mean HP by number of cylinders and origin",
31
+ )
32
+
33
+ # change labels in legend
34
+ l2 = bs_sns_get_legend(g2)
35
+ labels2 = ["USA", "Japan", "Europe"]
36
+ for t, lab in zip(l2.texts, labels2, strict=True):
37
+ t.set_text(lab)
@@ -0,0 +1,33 @@
1
+ """examples using my sklearn code"""
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from sklearn.preprocessing import PolynomialFeatures, StandardScaler
5
+
6
+ from bs_python_utils.sklearn_utils import skl_npreg_lasso
7
+
8
+
9
+ n_obs = 10000
10
+ X1 = -2.0 + 3.0 * np.random.uniform(size=n_obs)
11
+ X2 = np.random.normal(loc=1.0, scale=2.0, size=n_obs)
12
+ y = X1 * X2 * X2 / 100.0 - (X1 / 5.0 - X2 / 3.0) ** 3 + np.random.normal(size=n_obs)
13
+
14
+ X = np.column_stack((X1, X2))
15
+
16
+ plt.style.use("seaborn")
17
+
18
+ degree = 10
19
+ stdsc = StandardScaler()
20
+ sfit = stdsc.fit(X)
21
+ X_scaled = sfit.transform(X)
22
+ pf = PolynomialFeatures(degree)
23
+ # Create the features and fit
24
+ X_poly = pf.fit_transform(X_scaled)
25
+
26
+ y_pred = skl_npreg_lasso(y, X, alpha=0.001)
27
+
28
+ plt.clf()
29
+
30
+ ax = plt.axes()
31
+ ax.scatter(y, y_pred)
32
+ ax.plot(y, y, "-r")
33
+ plt.show()
@@ -0,0 +1,239 @@
1
+ """Utility functions for pandas """
2
+
3
+ from itertools import product
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from bs_python_utils.bsnputils import bs_error_abort, check_vector_or_matrix
9
+ from bs_python_utils.bsutils import print_stars
10
+
11
+
12
+ def bspd_print(
13
+ df: pd.DataFrame,
14
+ s: str | None = "",
15
+ max_rows: int | None = None,
16
+ max_cols: int | None = None,
17
+ precision: int | None = None,
18
+ ) -> None:
19
+ """Pretty-prints a data frame
20
+
21
+ Args:
22
+ df: any data frame
23
+ s: an optional title string
24
+ max_rows: maximum number of rows to print (all by default)
25
+ max_cols: maximum number of columns to print (all by default)
26
+ precision: of numbers. 3 digits by default.
27
+
28
+ Returns:
29
+ nothing.
30
+ """
31
+
32
+ print_stars(s)
33
+ with pd.option_context(
34
+ "display.max_rows",
35
+ max_rows,
36
+ "display.max_columns",
37
+ max_cols,
38
+ "display.precision",
39
+ precision,
40
+ ):
41
+ print(df)
42
+
43
+
44
+ def bspd_cross_products(
45
+ df: pd.DataFrame,
46
+ l1: list[str],
47
+ l2: list[str] | None = None,
48
+ with_squares: bool | None = True,
49
+ ) -> pd.DataFrame:
50
+ """Returns a DataFrame with cross-products of the variables of `df`
51
+ whose names are in `l1` and `l2`.
52
+
53
+ Args:
54
+ df: any data frame
55
+ l1: a list of names of variables that belong to `df`
56
+ l2: ibidem; `l1` by default
57
+ with_squares: if `False`, we drop the squares. `True` by default.
58
+
59
+ Returns:
60
+ the data frame of cross-products with concatenated names.
61
+ """
62
+
63
+ lp2 = l1 if l2 is None else l2
64
+ l12 = list(product(l1, lp2))
65
+ cross_pairs = [[x[0], x[1]] for x in l12 if x[0] != x[1]]
66
+ unique_pairs = []
67
+ for _i, c in enumerate(cross_pairs):
68
+ print(c)
69
+ c_ordered = c if c[0] < c[1] else list(reversed(c))
70
+ print(c_ordered)
71
+ if c_ordered not in unique_pairs:
72
+ unique_pairs.append(c_ordered)
73
+ print(unique_pairs)
74
+
75
+ col_names = sorted([(x[0], x[1], f"{x[0]}*{x[1]}") for x in unique_pairs])
76
+
77
+ if with_squares:
78
+ col_names_squares = sorted(
79
+ [(x[0], x[1], f"{x[0]}**2") for x in l12 if x[0] == x[1]]
80
+ )
81
+ col_names += col_names_squares
82
+
83
+ df_cprods = pd.DataFrame(
84
+ {col_name: df[x0] * df[x1] for (x0, x1, col_name) in col_names}
85
+ )
86
+
87
+ return df_cprods
88
+
89
+
90
+ def _list_str(names: str | list[str], suffix: str = None) -> list[str]:
91
+ """make a list of strings with possibly the added suffix
92
+
93
+ Args:
94
+ names: a string or a list of strings
95
+ suffix: a string, if any
96
+
97
+ Returns:
98
+ a list of the strings in names, with the suffix if specified
99
+ """
100
+ if isinstance(names, str):
101
+ if suffix is not None:
102
+ return [names + suffix]
103
+ else:
104
+ return [names]
105
+ elif isinstance(names, list):
106
+ if suffix is not None:
107
+ return [name + suffix for name in names]
108
+ else:
109
+ return names
110
+ else:
111
+ bs_error_abort("names should be a string or a list of strings.")
112
+
113
+
114
+ def _check_colnames(col_names: str | list[str] | list[str | list[str]], n_T: int):
115
+ if not isinstance(col_names, list):
116
+ bs_error_abort("If T is a list, then col_names should be a list too.")
117
+ elif len(col_names) != n_T:
118
+ bs_error_abort(f"T has {n_T} elements but col_names has {len(col_names)}.")
119
+
120
+
121
+ def bspd_statsdf(
122
+ T: np.ndarray | list[np.ndarray],
123
+ col_names: str | list[str] | list[str | list[str]],
124
+ ) -> pd.DataFrame:
125
+ """
126
+ make a dataframe with columns from the array(s) in T and names from col_names
127
+
128
+ Args:
129
+ T: a list of n_T matrices or vectors with N rows, or a matrix or a vector with N rows
130
+ col_names: a list of n_T name objects; a name object must be a string or a list of strings,
131
+ with the names for the column(s) of the corresponding T matrix
132
+
133
+ Returns:
134
+ a dataframe with the named columns
135
+ """
136
+ if isinstance(T, list):
137
+ n_T = len(T)
138
+ _check_colnames(col_names, n_T)
139
+ shape_T = []
140
+ for i in range(n_T):
141
+ shape_T.append(T[i].shape)
142
+ set_nrows = {shape_i[0] for shape_i in shape_T}
143
+ if len(set_nrows) > 1:
144
+ bs_error_abort("All T arrays should have the same number of rows.")
145
+ big_T = T[0]
146
+ big_names = _list_str(col_names[0], suffix="_1")
147
+ for i in range(1, n_T):
148
+ big_T = np.column_stack((big_T, T[i]))
149
+ big_names.extend(_list_str(col_names[i], suffix=f"_{i+1}"))
150
+
151
+ df = pd.DataFrame(big_T, columns=big_names, copy=True)
152
+
153
+ else: # only one element in T
154
+ ndims_T = check_vector_or_matrix(T)
155
+ if ndims_T == 1:
156
+ if not isinstance(col_names, str):
157
+ bs_error_abort(f"T is a vector but col_names is {col_names}")
158
+ df = pd.DataFrame(T, columns=[col_names], copy=True)
159
+ elif ndims_T == 2:
160
+ N, K = T.shape
161
+ K2 = len(col_names)
162
+ if K2 != K:
163
+ bs_error_abort(f"T is {T.shape} but col_names has {K2} elements")
164
+ df = pd.DataFrame(T, columns=col_names, copy=True)
165
+
166
+ return df
167
+
168
+
169
+ def _check_names_n(col_names: list[str]) -> list[int]:
170
+ """
171
+ Tests that if a name in col_names ends with `_n`, with `n` an integer, then all names do
172
+
173
+ Args:
174
+ col_names: a list of names
175
+
176
+ Returns:
177
+ the list of the values of `n` if all names in the list end in `_n`; `[]` if none of them does.
178
+ Aborts otherwise.
179
+ """
180
+ underscore_n = []
181
+ ending_integers = []
182
+ for name in col_names:
183
+ split_name = name.split("_")
184
+ len_split = len(split_name)
185
+ if len_split > 1: # found at least one '_'
186
+ last_bit = split_name[-1]
187
+ try:
188
+ ending_int = int(last_bit)
189
+ underscore_n.append(True) # ends with '_n'
190
+ ending_integers.append(ending_int)
191
+ except ValueError:
192
+ underscore_n.append(False)
193
+ else:
194
+ underscore_n.append(False)
195
+
196
+ values_integers = set(ending_integers)
197
+ n_values_integers = len(values_integers)
198
+
199
+ if n_values_integers == 0 or all(
200
+ underscore_n
201
+ ): # none ends in '_n' or all end in '_n'
202
+ return list(values_integers)
203
+ else:
204
+ bs_error_abort(
205
+ "If a column name ends with '_n' where n is an integer, then all should."
206
+ )
207
+ return [] # for mypy
208
+
209
+
210
+ def bspd_prepareplot(df: pd.DataFrame) -> pd.DataFrame:
211
+ """
212
+ Args:
213
+ df: any dataframe whose column names either all end in '_n' for n an integer, or none does
214
+
215
+ Returns:
216
+ a properly melted dataframe for plotting, with columns 'Sample', 'Statistic', 'Value',
217
+ and 'Group' if there are several integers
218
+ """
219
+ # check the names of the columns
220
+ values_integers = _check_names_n(df.columns)
221
+ n_values_integers = len(values_integers)
222
+
223
+ df2 = df.copy()
224
+ df2["Sample"] = np.arange(df.shape[0])
225
+ dfm = pd.melt(
226
+ df2,
227
+ id_vars="Sample",
228
+ value_vars=list(df.columns),
229
+ var_name="Statistic",
230
+ value_name="Value",
231
+ )
232
+ if n_values_integers in [0, 1]:
233
+ return dfm
234
+ else: # at least two different groups of statistics
235
+ stat_group = dfm["Statistic"].str.split("_", n=1, expand=True)
236
+ dfm.drop(columns=["Statistic"], inplace=True)
237
+ dfm["Statistic"] = stat_group[0]
238
+ dfm["Group"] = stat_group[1]
239
+ return dfm
@@ -0,0 +1,74 @@
1
+ """
2
+ Cntains various `scikit-learn` utility programs.
3
+ """
4
+
5
+ from itertools import cycle
6
+ from typing import cast
7
+
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ from sklearn.linear_model import Lasso, lasso_path
11
+ from sklearn.preprocessing import PolynomialFeatures, StandardScaler
12
+
13
+
14
+ def skl_npreg_lasso(
15
+ y: np.ndarray, X: np.ndarray, alpha: float, degree: int = 4
16
+ ) -> np.ndarray:
17
+ """
18
+ Lasso nonparametric regression of `y` over polynomials of `X`
19
+
20
+ Args:
21
+ y: shape `(nobs)`
22
+ X: shape `(nobs, nfeatures)`
23
+ alpha: Lasso penalty parameter
24
+ degree: highest total degree
25
+
26
+ Returns:
27
+ the `(nobs)` array `E(y\\vert X)` over the sample
28
+ """
29
+
30
+ # first scale the X variables
31
+ stdsc = StandardScaler()
32
+ sfit = stdsc.fit(X)
33
+ X_scaled = sfit.transform(X)
34
+ pf = PolynomialFeatures(degree)
35
+ # Create the features and fit
36
+ X_poly = pf.fit_transform(X_scaled)
37
+ # now run Lasso
38
+ reg = Lasso(alpha=alpha).fit(X_poly, y)
39
+ expy_X = reg.predict(X_poly)
40
+ return cast(np.ndarray, expy_X)
41
+
42
+
43
+ def plot_lasso_path(y: np.ndarray, X: np.ndarray, eps: float = 1e-3) -> None:
44
+ """
45
+ plot Lasso coefficient paths
46
+
47
+ Args:
48
+ y: shape `(nobs)`
49
+ X: shape `(nobs, nfeatures)`
50
+ eps: length of path
51
+
52
+ Returns:
53
+ plots the paths
54
+ """
55
+ # Compute paths
56
+ print("Computing regularization path using the lasso...")
57
+ alphas_lasso, coefs_lasso, _ = lasso_path(X, y, eps)
58
+
59
+ plt.clf()
60
+ # Display results
61
+ plt.figure(1)
62
+ colors = cycle(["b", "r", "g", "c", "k"])
63
+ neg_log_alphas_lasso = -np.log10(alphas_lasso)
64
+ for coef_l, c in zip(coefs_lasso, colors, strict=True):
65
+ plt.plot(neg_log_alphas_lasso, coef_l, c=c)
66
+
67
+ plt.xlabel("-Log(alpha)")
68
+ plt.ylabel("coefficients")
69
+ plt.title("Lasso Paths")
70
+ plt.axis("tight")
71
+
72
+ plt.show()
73
+
74
+ return
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023, Bernard Salanie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.