Is there a python library for representing conditionals of two values as a matrix/table?

Viewed 35

We're trying to figure out a way to easily pull values from what I guess I would describe as a grid of conditional statements. We've got two variables, x and y, and depending on those values, we want to pull one of (something1, ..., another1, ... again1...). We could definitely do this using if statements, but we were wondering if there was a better way. Some caveats: we would like to be able to easily change the bounds on the x and y conditionals. The problem with a bunch of if statements is that it's not very easy to compare the values of those bounds with the values in the example table below.

Example:

enter image description here So if x = 4% and y = 30%, we would get back another1. Whereas if x = 50% and y = 10%, we would get something3.

Overall two questions:

  1. Is there a general name for this kind of problem?
  2. Is there an easy framework or library that could do this for us without if statements?
3 Answers

I didn't hear about any name.
If (ha-ha) it should be more conceptually close to you, I might suggest that you create two mapper functions that would map x and y values to the categories of your contingency table.

map_x = lambda x: 0 if x < 0.05 else 1 if x < 0.1 else 2
map_y = lambda y: 0 if y < 0.2 else 1 if y < 0.5 else 2
df.iloc[map_x(x), map_y(y)]

If you have just a handful of conditionals then you may define two lists with the upper bounds, and use a simple linear search:

x_bounds = [0.05, 0.1, 1.0]
y_bounds = [0.2, 0.5, 1.0]

def linear(x_bounds, y_bounds, x, y):
    for i,xb in enumerate(x_bounds):
        if x <= xb:
            break
    for j,yb in enumerate(y_bounds):
        if y <= yb:
            break
    return i,j

linear(x_bounds, y_bounds, 0.4, 3.0) #(0,1)

If there are many conditionals a binary search will be better:

def binary(x_bounds, y_bounds, x, y):
    lower = 0
    upper = len(x_bounds)-1
    while upper > lower+1:
        mid = (lower+upper)//2
        if x_bounds[mid] < x:
            lower = mid
        elif x_bounds[mid] >= x:
            if mid > 0 and x_bounds[mid-1] < x:
                xmid = mid
                break
            else:
                xmid = mid-1
                break
        else:
            upper = mid
    lower = 0
    upper = len(y_bounds)-1
    while upper > lower+1:
        mid = (lower+upper)//2
        if y_bounds[mid] < y:
            lower = mid
        elif y_bounds[mid] >= y:
            if mid > 0 and y_bounds[mid-1] < y:
                ymid = mid
                break
            else:
                ymid = mid-1
                break
        else:
            upper = mid
    return xmid,ymid

binary(x_bounds, y_bounds, 0.4, 3.0) #(0,1)

Even though Pandas is not really made for this kind of usage, with function aggregation and boolean indexing it allows for an elegant-ish solution for your problem. Alternatively, constraint-based programing might be an option (see python-constraint on pypi).

  1. Define the constraints as functions.
x_constraints = [lambda x: 0 <= x < 5, 
                 lambda x: 5 <= x < 10, 
                 lambda x: 10<= x < 15,
                 lambda x: x >= 15
                ]
y_constraints = [lambda y: 0 <= y < 20, 
                 lambda y: 20 <= y < 50, 
                 lambda y: y >= 50]

x = 15
y = 30
  1. Now we want to make two dataframes: One that only holds the x-values, and another that only holds the y-values where number of columns = number of x-constraints and number of rows = number of y-constraints.
import pandas as pd

def make_dataframe(value):
    return pd.DataFrame(data=value, 
                        index=range(len(y_constraints)), 
                        columns=range(len(x_constraints)))

x_df = make_dataframe(x)
y_df = make_dataframe(y)

The dataframes look like this:

>>> x_df 
    0   1   2   3
0  15  15  15  15
1  15  15  15  15
2  15  15  15  15
>>> y_df
    0   1   2   3
0  30  30  30  30
1  30  30  30  30
2  30  30  30  30
  1. Next, we need the dataframe label_df that holds the possible outcomes. The shape must match the dimension of x_df and y_df above. (What's cool about this is that you can store the data in a CSV-file and directly read it into a dataframe with pd.read_csv if you wish.)
label_df = pd.DataFrame([[f"{w}{i+1}" for i in range(len(x_constraints))] for w in "something another again".split()])
>>> label_df
            0           1           2           3
0  something1  something2  something3  something4
1    another1    another2    another3    another4
2      again1      again2      again3      again4
  1. Next, we want to apply the x_constraints to the columns of x_df, and the y_constraints to the rows of y_df. .aggregate takes a dictionary that maps column or row names to functions {colname: func}, which we construct inline using dict(zip(...)). axis=1 means "apply the functions row-wise".
x_mask = x_df.aggregate(dict(zip(x_df.columns, x_constraints)))
y_mask = y_df.aggregate(dict(zip(y_df.columns, y_constraints)), axis=1)

The result are two dataframes holding boolean values, and ideally, there should be exactly one column in x_mask and one row in y_mask that's all True, e.g.

>>> x_mask
       0      1      2     3
0  False  False  False  True
1  False  False  False  True
2  False  False  False  True
>>> y_mask
       0      1      2      3
0  False  False  False  False
1   True   True   True   True
2  False  False  False  False

If we combine them with bit-wise and &, we get a boolean mask with exactly one True value.

>>> m = x_mask & y_mask
>>> m
       0      1      2      3
0  False  False  False  False
1  False  False  False   True
2  False  False  False  False
  1. Use m to select the target value from label_df. The result df is all NaN except one value, which we extract with df.stack().iloc[0]:
>>> df = label_df[m]
     0    1    2         3
0  NaN  NaN  NaN       NaN
1  NaN  NaN  NaN  another4
2  NaN  NaN  NaN       NaN
>>> df.stack().iloc[0]
'another4'

And that's it! It should be very easy to maintain, by just changing the list of constraints and adapting the possible outcomes in label_df.

Related