13

I'm trying to implement the simplex method in Python so I need to use the Gaussian elimination on arrays. Very often fractions come up and for more clarity and precision I would like to keep the fractional form instead of using floats. I know the 'fractions' module but I'm struggling to use it. I wrote my code using this module but the arrays are always returned with floats. Isn't it possible to print an array with fractions inside ? On this basic example :

>>> A
array([[-1.,  1.],
       [-2., -1.]])
>>> A[0][0] = Fraction(2, 3)
>>> A
array([[ 0.66666667,  1. ],
       [-2.        , -1. ]])

I would like to have

array([[2/3,    1. ],
       [-2.,   -1. ]])

It seems numpy always switches to floats

ddejohn
  • 8,775
  • 3
  • 17
  • 30
Jkev
  • 177
  • 1
  • 2
  • 8
  • 4
    If you want to work with matrices of exact rational numbers, [sympy](http://docs.sympy.org/dev/tutorial/matrices.html) would probably serve you better. – user2357112 Mar 03 '17 at 11:37
  • Thank you for your answer but I won't use sympy since I already started my code with numpy. I didn't know sympy so I keep that in mind for a next code ! – Jkev Mar 03 '17 at 14:01
  • I tested sympy on matrices and it's very very slow: https://stackoverflow.com/questions/45796747/are-sympy-matrices-really-that-slow – Wikunia Aug 21 '17 at 14:45

2 Answers2

10

You can also convert the entire array to an object array of Fraction objects, by abusing the element-wise conversion of numpy arrays under arithmetic operations. (Note: this requires the original array to be an integer array, since arithmetic between floats and Fractions produce floats.)

>>> A = np.array([[-1,  1],[-2, -1]])
array([[-1,  1],
       [-2, -1]])
>>>
>>> A.dtype
dtype('int64')
>>>
>>> A = A + Fraction()
>>> A
array([[Fraction(-1, 1), Fraction(1, 1)],
       [Fraction(-2, 1), Fraction(-1, 1)]], dtype=object)

With the array in this format, any further arithmetic performed will be over elements of type Fraction.

Edit: disclaimers

As mentioned by @Hi-Angel in the comments, there are a number of NumPy/SciPy functions (e.g., np.linalg.inv) that expect input arrays to use a primitive dtype (e.g., int32, float64, etc.); these functions tend to be C/Cython-optimized routines that only work on C-primitives. And because fractions.Fraction is a Python object, these functions will not work on arrays of Fractions.

And as mentioned elsewhere, even the functions that do work on Fraction arrays will run notably slower on them, compared to running on NumPy arrays of primitive dtypes.

However, if you just need a custom numeric object for your application, like the arbitrary-precision rational type Fraction or the base-10 floating-point type decimal.Decimal, and want the convenience of e.g. element-wise operations on arrays, you CAN use NumPy arrays to achieve that using the method above or similar methods.

But it's not as fast or well-supported as using arrays of primitives, so personally if I don't NEED a custom number type I just use float64s or int64s.

CrepeGoat
  • 2,315
  • 20
  • 24
  • Is there no other way to do it? I tried using `.astype` but it failed. Can we not cast the array directly somehow? This hack is great and it works. – Param Siddharth Apr 19 '21 at 16:05
  • 1
    @Param I assume you mean that you tried `A.astype(Fraction)`? If so, then you're right that does NOT work, because numpy has no dtype that is comparable to python's built-in `Fraction` type. – CrepeGoat Apr 20 '21 at 16:54
  • 1
    a more explicit method, if you need one, could be to do something like `np.vectorize(Fraction)(A)`: this will call `Fraction` on each element of the array and return the result. This also automatically converts the result array to an object-typed array. (However I hear that `np.vectorize` is not always especially performant...) – CrepeGoat Apr 20 '21 at 16:57
  • Well, I mean, it does seem to work as in, it stores the array as fractions. But it looks useless, because I can't do anything with it: e.g. when I try to calculate the inverse in fractions, I get: `np.linalg.inv(A)` → `numpy.core._exceptions._UFuncInputCastingError: Cannot cast ufunc 'inv' input from dtype('O') to dtype('float64') with casting rule 'same_kind'` – Hi-Angel Aug 20 '22 at 20:59
  • @Hi-Angel that’s good to note for sure: the fraction number type (and Python objects in general) does not have first-class support in numpy. so some numpy functions that expect an array of numpy primitives may fail – CrepeGoat Aug 22 '22 at 03:58
  • On the other hand, I’ve written a lot of numpy code that doesn’t use np.linalg.inv. So personally I think saying it’s “useless”, especially if in reference to a general use case, is a little strong. – CrepeGoat Aug 22 '22 at 04:01
  • @CrepeGoat so you're saying some functions work and some doesn't? That's good to know, thanks! That means one still could use numpy with fractions, just reimplement those functions that doesn't work. I think, it might be useful to note that in the answer, to save some potential confusion. – Hi-Angel Aug 22 '22 at 06:25
  • @Hi-Angel I just added that section; does it make sense and explain what you were asking? – CrepeGoat Aug 22 '22 at 19:17
  • Yeah, thanks, that clears that up. ⁺¹ from me – Hi-Angel Aug 22 '22 at 20:28
7

Since Fractions are not a native NumPy dtype, to store a Fraction in a NumPy array you need to convert the array to object dtype:

import numpy as np
from fractions import Fraction

A = np.array([[-1.,  1.],
              [-2., -1.]])   # <-- creates an array with a floating-point dtype (float32 or float64 depending on your OS)
A = A.astype('object')
A[0, 0] = Fraction(2,3)
print(A)

prints

[[Fraction(2, 3) 1.0]
 [-2.0 -1.0]]

PS. As user2357112 suggests, you might be better off using sympy if you wish to use rational numbers. Or, just represent the matrix as a list of lists. There are no speed advantages to using NumPy if your arrays are of object dtype.

import sympy as sy

A = [[-1.,  1.],
     [-2., -1.]]
A[0][0] = sy.Rational('2/3')
print(A)

prints

[[2/3, 1.0], [-2.0, -1.0]]
Community
  • 1
  • 1
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677