AST416 Astronomide Sayısal Çözümleme - II

Ders - 02a Bilgisayarda Sayıların Temsili ve Yuvarlama Problemleri ile Python Çözümleri

Doç. Dr. Özgür Baştürk
Ankara Üniversitesi, Astronomi ve Uzay Bilimleri Bölümü
obasturk at ankara.edu.tr
http://ozgur.astrotux.org

Yuvarlama Yanlışları

Yuvarlama konusunun ayrıntlı olarak anlatıldığı Hata Analizi dersinde ayrıntılı olarak verildiği gibi Yuvarlama yanlışları temelde iki problemden kaynaklanır:

  1. $\pi$, $e$, $\sqrt{7}$ gibi sayılar sabit sayıda anlamlı rakamla temsil edilemezler. Bu nedenle bilgisayarlar da bu sayıları tam olarak temsil edemez.

  2. Bilgisayarlar ikilik sayı sistemini kullandıklarından onluk sayı sistemine dayalı tüm sayıları aynı duyarlılıkta temsll edemezler.

Yuvarlama işlemi kaynaklı hatalar sayıların bilgisayarda tutulma şekline doğrudan bağlıdır. 10 parmağımız olduğu için onluk sayı sistemi biz insanlar için “doğal” olandır. Bir bilgisayar ise sadece 2 parmağı olan bir varlık gibi düşünülebilir. Bu nedenle ikilik sayı sistemi de onun için “doğal” olandır.

Herhangi bir tamsayı onluk ve ikilik sayı sistemlerinde aşağıdaki şekillerde çözümlenir (Chapra & Canale 2009).

In [1]:
from IPython.display import Image
Image(filename='images/sayi_sistemleri.png', width=400)
Out[1]:

Bu durum her iki sayı sisteminde ve diğer tüm sayı sistemlerinde sayıların mükemmel temsil edilemiyor olmasına yol açar. Buna ek olarak bilgisayarlar sınırlı kapasiteleri nedeniyle sadece belirli bir sayı aralığını temsil edebilirler. Örneğin 16 bitlik bir sayı sistemi varsayılan olarak -32768 ile 32767 arasındaki tam sayıları saklayabilir. 16 bitlik CCD dedektörlerde algılanan fotonların ADU biriminden karşılığının 0-65535 arasında olbilmesinin nedeni de budur. Foton algılama işlemi doğası gereği bir "sayım" işlemi olduğundan negatif sayılara ihtiyaç duyulmaz.

In [2]:
from IPython.display import Image
Image(filename='images/sayi_sistemleri_16bit.png', width=400)
Out[2]:

Yukarıdaki şekilde şematik olarak gördüğünüz 16 bit ikilik sayı sisteminde, her bir basamak sadece 0 veya 1 değerini alabilir ve toplamda 16 basamak vardır. 1. basamak işaret için ayrılır ve 0 iki kere sayılmaz. En baştaki 0: pozitif, 1: negatif sayıları ifade eder. Bu durumda yazılabilecek en küçük sayı $1111111111111111$ (bu sayının onluk sayı sistemindeki karşılığı -32767 olduğu halde 0'ı iki kez saymamak için onu pozitif sayılara dahil edip saymaya -1'den başladığımız için -32768) olurken en büyük sayı $0111111111111111$ (32767) olur.

Başa Dön

Kayan Noktalı Sayılar

Bilgisayarlar tam sayılar dışındaki sayıları göstermek üzere "kayan noktalı" sayıları (ing. floating point numbers) kullanırlar. Aşağıda kayan noktaları sayıların hafızada saklanma şeklini örnekleyen bir şekil bulunmaktadır. Herhangi bir sayı bu şekilde m: mantis, b: taban, e: üssü göstermek üzere

$$ m~b^e $$

şeklinde ifade edilir.

In [3]:
from IPython.display import Image
Image(filename='images/sayi_sistemleri_floatingpoint.png', width=400)
Out[3]:

Örneğin onluk sayı sistemindeki $156.78$ sayısını düşünelim. Bu sayıyı bilimsel gösterimde (ing. scientific notation) şu şekilde ifade edebiliriz: $0.15678 x 10^{3}$.

Eğer mantis başında gereksiz $0$ sayıları varsa normalize edilebilir. Örneğin, $1 / 34 = 0.029411765...$ sadece 4 basamağa izin verilen 10'luk bir kayan sayı sisteminde $0.0294 x 10^0$ şeklinde saklanabilir. Ancak bu şekilde noktadan sonraki 0 nedeniyle 5. basamaktaki $1$ sayısını kaybetmiş oluruz. Bunu sayıyı $0.2941 x 10^{-1}$ şeklinde göstererek aşabiliriz. Noktanın bu şekilde kaydırılması bu sayıların "kayan noktalı sayılar” (ing. floating point numbers) olarak adlandırılmış olmasının da kaynağıdır. Böylece noktadan sonra gelen gereksiz 0'a da izin verilmemiş olur. Bu işleme normalizasyon adı verilir ve sayılarda anlamsız rakamların elenmesi işlevini de görür.

Normalizasyonun doğal bir sonucu mantis (m) için limitli bir değer aralığına sahip olunmasıdır. b, tabanı; m, mantisi göstermek üzere

$$\frac{1}{b} \le m \lt 1$$

Örneğin onluk sayı sisteminde

$$0.1 \le m \lt 1$$

iken ikilik sayı sisteminde

$$ 0.5 \le m \lt 1$$

Örnek: Yedi bitlik ikilik bir sayı sistemini ele alalım (Şekil Chapra & Canale 2009'dan alınmış ve Türkçeleştirilmiştir) ve bu sayı sisteminde ifade edilebilecek en büyük ve en küçük sayıları hesaplamak istiyor olalım.

In [4]:
from IPython.display import Image
Image(filename='images/sayi_sistemleri_7bit.png', width=300)
Out[4]:

Bu sistemde ifade edebileceğimiz en küçük poziftif sayı $011100$ 'dir. bu sayının onluk sayı sistemindeki karşılığı $m = 1x2^{-1} + 0x2^{-2} + 0x2^{-3} = 0.5$, $b=2$ , $e = -(1x2^0 + 1x2^1) = -3$ olduğundan

$$ (1x2^{-1} + 0x2^{-2} + 0x2^{-3}) x 2^{-(1x2^1+1x2^0)} = 0.5 x 2^{-3} = 0.0675 $$

sayısıdır.

En büyük poziftif sayı ise $0011111$ sayısı olup onluk sayı sistemindeki karşılığı $m = 1x2^{-1} + 1x2^{-2} + 1x2^{-3} = 0.875$, $b=2$ , $e = 1x2^0 + 1x2^1 = 3$ olduğundan

$$ 1x2^{-1} + 1x2^{-2} + 1x2^{-3}) x 2^{1x2^1 + 1x2^0} = 0.875 x 2^{3} = 7 $$

sayısıdır.

Aslında bu sayı sisteminde daha küçük mantisler yazabiliriz ($000$, $001$, $010$, $011$) ancak normalizsyon gereği noktadan sonraki gereksiz $0$'ları atmaya yönelik bir işlem uygulanacağından buna izin verilmez. Dolayısı ile en küçük olarak ifade edilebilien bu sayıyı aşağıdaki sayılar takip eder:

$$ 0111101 = (1 x 2^{−1} + 0 x 2^{−2} + 1x2^{−3}) x 2^{−3} = (0.078125)_{10} $$$$ 0111101 = (1 x 2^{−1} + 1 x 2^{−2} + 0x2^{−3}) x 2^{−3} = (0.093750)_{10} $$

$$ 0111101 = (1 x 2^{−1} + 1 x 2^{−2} + 1x2^{−3}) x 2^{−3} = (0.109375)_{10} $$

Bu sayı sistemiyle $0.0675$ sayısından küçük pozitif sayıların temsil edilememesinin yanı sıra her sayının da temsil edilemediğine dikkat ediniz. Sayılar $0.015625$ aralıkla artmakta, bu artışın altındaki artışlarla ulaşılacak sayılar gösterilememektedir! Bu noktadan sonra sayıyı büyütmek için üssü büyütmeliyiz!

$$ 0110100 = (1 x 2^{−1} + 0 x 2^{−2} + 0x2^{−3}) x 2^{−2} (0.125000)_{10} $$$$ 0110101 = (1 x 2^{−1} + 0 x 2^{−2} + 1x2^{−3}) x 2^{−2} = (0.156250)_{10} $$

...
...
...
$$ 0011111 = (1 x 2^{−1} + 0 x 2^{−2} + 1x2^{−3}) x 2^{3} = (7)_{10} $$

Görüldüğü üzere sayılar büyüdükçe aralarındaki farklar da artmakta ve tam olarak temsil edilemeyen sayıların sayısı da buna bağlı olarak artmaktadır. Ayrıca 7 sayısının üstündeki sayılar da bu sayı sisteminde temsil edilememektedir. 7 bitlik sayı sistemi kuşkusuz modern bilgisayarlarda kullanılan 32 bit ve 64 bitlik sayı sistemleriyle karşılaştırıldığında son derece basit ve kullanışsız ve bir sistemdir. Ancak onlarda karşılaşılan güçlükleri örneklemek açısından faydaldır.

Başa Dön

Sayı Sistemlerinde Temsil Sorunları ve Sonuçları

Yukarıdaki örneklerden çıkarsanması gereken dört temel sonuç:

  1. Sadece belirlii bir aralıktaki sayılar temsil edilebilir. Tıpkı tam sayıda olduğu gibi ($-32768 – 32767$) kayan noktalı sayı gösteriminde de çok büyük pozitif ve çok küçük negatif sayıların temsili sorunu vardır. Bu sınırların dışına çıkmaya kalkarsanız taşma (overflow) hatası alırsınız.

  2. Normalizasyon nedeniyle çok küçük pozitif sayıların gösterimiyle de ilgili bir problem yaşanır. Bu problem 0'dan sonra ilk temsil edilebilen sayıya kadar büyük bir boşluğun (underflow) oluşmasına neden olur.

  3. Tüm nicelikler temsil edilemezler! Bu nedenle hassasiyet de sınırlıdır. Açık ki irrasyonel sayılar tam olarak temsil edilemezler. Ayrıca temsil edilebilen küme dışında kalan rasyonel sayılar da tam olarak temsil edilemezler. Bu sorun kuantizasyon hatası (quantization error) olarak bilinen hataya yol açar.

  4. Temsil edilmek istenen sayı büyüdükçe temsil edilebilen sayılar arasındaki uzaklığın da artmasıdır (64 bitlik sayı sisteminde $1.0$ ile $2.0$ arasında $8~388~607$ tane kayan noktalı sayı varken $1023$ ile $1024$ arasında sadece $8191$ tane vardır).

Çözüm: Temsil edemediğniz basamakları ya keser atarsınız (ing. truncation) ya da yuvarlarsınız (rounding)!

In [5]:
from IPython.display import Image
Image(filename='images/sayi_sistemleri_yuvarlama_kesme.png', width=500)
Out[5]:

Kesme ve Yuvarlama İşlemleri

“Kırpma” ya da kesme (truncation, chopping) işlemi uygulanırsa, şekilden de görüleceği gibi sondan atılan $\Delta x$ kadar hata yapılmış olur. Bu durumda yapılan hata her zaman pozitiftir. Örneğin $\pi = 3.14159265358...$ sayısına bakalım. Sayımız 6. basamaktan sonra temsil edilemiyor olsun. Bu durumda kesme işi uygularsak sayı $\pi ~ 3.141592$ şeklinde ifade edilir ve hatası $|\epsilon_t| = 0.00000065...$ olur. Oysa ki yuvarlama işlemi uygulanmış olsaydı sayı $\pi ~ 3.141593$ şeklinde ifade edilir ve hatası $\epsilon_t = 0.00000035...$ olurdu. Dolaysı ile kesme işleminde hatanın üst sınırı $\Delta x$ iken, yuvarlamada $\Delta x / 2$'dir. Ayrıca yuvarlama işleminden gelen hatada yanlılık da daha azdır; pozitif olabileceği gibi negatif de olabilir. Ancak yuvarlama işlemi ek kod gerektirir. Bu ek koddan kaçınmak için, modern bilgisayarların oldukça büyük ve küçük sayıları saklayabiliyor olması gerekçesiyle, kesme işlemine gidilir.

Bir başka problem de temsil edilmek istenen sayı büyüdükçe temsil edilebilen sayılar arasındaki uzaklığın da artmasıdır. Dolayısı ile kuantizasyon hatası sayının büyüklüğüne bağlıdır. Bu ilişki kesme işlemi için $\Delta x / x \le \epsilon$, yuvarlama işlemi için $\Delta x / x \lt \epsilon / 2$ şeklinde ifade olunur. Burada $\epsilon$ sistem (ya da makine) epsilonu olarak bilinir ve $\epsilon = b^{1 - t}$ olarak ifade edilir. t, mantisteki anlamlı rakam sayısını göstermektedir.

Örnek: 7 bitlik hayali sayı sistemimiz için makine epsilonu değerini hesaplayalım:

Sistemimizin tabanı 2 olduğundan $b = 2$ 'dir. Mantisteki anlamlı rakam sayısı $t = 3$ 'tür. Bu durumda makine epsilonu $\epsilon = b^{1 - t} = 2^{-2} \rightarrow \epsilon = 0.25$ olarak bulunur. Dolayısı ile kesme işlemi uygulandığında yapılabilecek maksimum göreli hata da bu kadar olacaktır. Sayılar büyüdükçe kesme işlemi sonucu yapılan hata büyüyor olsa da sayının mutlak değeri de büyüdüğünden göreli hata değeri bu üst sınırın altında kalmaya devam edecektir. Örneğimizde maksimum hata $(0.125000)_{10}$ ile $(0.156250)_{10}$ arasında gerçekleşmektedir. Dolayısı ile hata $(0.156250 – 0.125000) / 0.125000 = 0.25$ olur.

Önemli Sonuç: Bu problem özellikle iki kayan noktalı sayıyı karşılaştırırken karşımıza çıkar. Matematiksel olarak birbirine eşitmiş gibi görünen iki ifade kesme ya da yuvarlama nedeniyle birbirinden farklı iki kayan noktalı sayıyla sonuçlanabilir. Doğrusu iki kayan noktalı sayıyı karşılaştırmak yerine, aralarındaki farkın mutlak değerini yeterince küçük seçilmiş bir tolerans değeriyle karşılaştırmak; bu tolerans değerini seçerken ise programı sistem bağımsız hale getirmek üzere makine epsilonundan faydalanmaktır!

$a$ ve $b$ kayan noktalı sayıları için; $a == b$ ya da $a != b$ yerine $abs(a – b) \gt tolerans $ (örn. $2~\epsilon$) gibi bir karşılaştırma yapmak daha doğrudur.

Başa Dön

Makine Epsilonu

Makine epsilonu aşağıdaki basit kodla hesaplanabileceği gibi her sistemin makine epsilionu sys.float_info.epsilon ile de öğrenilebilir.

In [6]:
epsilon = 1.
while epsilon + 1 > 1:
    epsilon /= 2.
epsilon *= 2 
print("Makine epsilonu", epsilon)
Makine epsilonu 2.220446049250313e-16
In [7]:
import sys
print(sys.float_info.epsilon)
2.220446049250313e-16

7 bitlik hayali sayı sistemi örneğimiz konuyu daha iyi kavratabilmek amacıyla oldukça abartılı limitlere sahiptir. Modern bilgisayarlar kayan noktalı sayı hassasiyeti konusunda çok daha başarılıdır. IEEE standardını takip eden bilgisayarlar sayının işareti için 1 bit, üs için 8 bit, mantis için 24 bit (toplam 32 bit) kullanırlar. Normalizasyon nedeniyle bu bitlerini birincisi her zaman $1$ olduğundan geriye kalan 23 bitle $10^{-38} - 10^{39}$ arasındaki sayılar ifade edilebilmektedir.

Daha da fazla hassasiyete ihtiyaç duyulduğunda pek çok sistem ek bir hassasiyet seçeneği sunar: Çifte Duyarlılık (Double Precision). 15-16 ek basamağın sağlandığı bu seçenekle $10^{-308} - 10^{308}$ arasındaki sayıları ifade edilebilecek duyarlılığa kadar inilebilmektedir.

Tabi bu ek duyarlılık işlemci ve RAM üzerine daha çok yük ve dolayısı ile daha uzun çalışma dezavantajıyla birlikte gelir, bu nedenle uygun yerlerde, dikkatli ve seçilerek kullanılması gereklidir!

Başa Dön

Aritmetik İşlemlerin Getirdiği Kesme ve Yuvarlama Yanlışları

1 Toplama ve Çıkarma

Toplama ve çıkarma işlemleri küçük sayının mantisi her iki sayının üssü eşit olacak şekilde düzenlendikten sonra yapılır.

Örnek 1. $1.5571$ ile $0.4381$ sayılaırnı topluyor olalım. Bu sayılar alt alta yazılırken noktanın aynı yere getirilmesine dikkat ediliyor.

$$ 0.1557~~~~10^1 $$$$ 0.004381~~~10^1 $$$$ +----------- $$$$ 0.160081~~~10^1 $$

Sayı daha az sayıda basamak içeren (4) sayı kadar rakam içereceğinden bunun için kesilir ve

$$ 0.1600 x 10^1 $$

şeklinde ifade edilir.

Örnek 2. $364.1$'den $268.6$'yı çıkarıyor olalım.

$$ ~0.3641~~~~10^2 $$$$- 0.2686~~~~10^2 $$$$ ------------ $$$$ ~0.0955~~~~10^2 $$

Normalizasyon gereği sayı,

$$ 0.9550 x 10^1 $$

şeklinde ifade edilir.

Örnek 3.

$$ ~0.7642~~~~10^3 $$$$- 0.7641~~~~10^2 $$$$ ------------ $$$$ ~0.0001~~~~10^3 $$

Normalizasyon gereği sayı,

$$ 0.1000 x 10^0 = 0.1$$

şeklinde ifade edilir.

2 Çarpma ve Bölme

Çarpma ve bölme işlemleri görece daha kolaydır: Mantisler çarpılır, üsler toplanır! Pek çok sistem ara adımları çift duyarlılıkta saklayarak ilerler.

Örnek:

$$ 0.6313~10^3~x~0.6423~10^{-1} = 0.08754549~10^2 $$

Normalizasyon ve kesme işlemleri sonrası

$$ 0.8754549~10^1 \Rightarrow 0.8754~10^1$$

3 Büyük ve Küçük Sayıyla Toplama Çıkarma

Kesme / yuvarlama nedeniyle bazen işlem yapılmamış gibi bir sonuçla dahi karşılaşılabilir.

$$ 0.4000~~~~10^4 $$$$ 0.0000001~~~10^4 $$$$ +----------- $$$$ 0.4000001~~~10^4 $$

Kesme işlemi sonrası

$$ 0.4000~10^4 $$
In [8]:
x = 0.000001
toplam = 0.
for i in range(0, 1000000, 1):
    toplam += x
print("Toplam: ", toplam)
Toplam:  1.000000000007918

4 Yakın İki Sayıyı Çıkarma

Birbirlerine çok yakın iki sayı çıkarılmaya çalışıldığında sayıların tam temsil edilememesi ve yuvarlama / kesme işlemi kaynaklı olan, ancak beklenmeyen sorunlarla karşılaşılabilir. Buna iyi bir örnek ikinci dereceden bir denklemin kökleri hesaplanırken karşılaşılabilecek olan aşağıdaki gibi bir sonuçtur.

$$ \frac{-b \pm \sqrt{b^2 - 4~a~c}}{2~a} $$
In [9]:
from math import sqrt
a = 1.
b = 3000.001
c = 3.
x1 = (-1*b - sqrt(b**2 - 4*a*c))/(2*a)
x2 = (-1*b + sqrt(b**2 - 4*a*c))/(2*a)
print("x1: ", x1)
print("x2: ", x2)
x1:  -3000.0
x2:  -0.0009999999999763531

Python'da Kayan Noktalı Sayılar

Görüldüğü gibi kayan noktalı sayılar ilgili olarak karşılaşılan en temel sorun onluk sayı sistemindeki bir sayının ikilik sayı sisteminde tam olarak ifade edilememesidir. Örneğin, $0.1$ sayısının ikilik sayı sistemindeki karşılığı $0.0001100110011001100110011001100110011001100110011...$

Python $repr()$ fonksiyonu ve Python promptu kayan noktalı bir sayıyı 17 basamak duyarlıılıkla gösterir. İstenirse sayı ekrana daha büyük ya da küçük bir duyarlılıkla da getirilebilir.

In [10]:
from math import pi 
print(format(pi, '.12g'))
print(repr(pi))
3.14159265359
3.141592653589793

Gerçekte bu bir ilüzyondur. Zira bu sayının tam bir gösterimini yapabilmek mümkün değildir. Ekrana bindirilen görüntü her zaman belirli duyarlılıkta bir yaklaşımdan ibarettir. Aşağıdaki örnek daha açıklayıcıdır.

In [11]:
print(.1 + .1 + .1 == .3)
print(round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1))
print(round(.1 + .1 + .1, 10) == round(.3, 10))
False
False
True

math modülü fonksiyonlarından fsum ise toplama işleminde yuvarlama kaynaklı hataların giderilmesini sağlar.

In [12]:
from math import fsum
print(sum([.1, .1, .1, .1, .1, .1, .1, .1, .1, .1]))
print(fsum([.1, .1, .1, .1, .1, .1, .1, .1, .1, .1]))
0.9999999999999999
1.0

Python'da bir kayan noktalı sayının hangi iki tam sayının oranı olarak ifade edildiğini veren bir metot bulunmaktadır. Kayan sayı nesnelerinin bir metodu olan as_integer_ratio().

In [13]:
print(3.5.as_integer_ratio())
from math import pi
print(pi.as_integer_ratio())
(7, 2)
(884279719003555, 281474976710656)

Bu fonksiyonu kullanarak 0.1 sayısının nasıl temsil edildiğine bakalım. 1/10 değil, çünkü 1/10 ikilik sayı sisteminde tam olarak gösterilemez!

In [14]:
 0.1.as_integer_ratio()
Out[14]:
(3602879701896397, 36028797018963968)

$fractions$ modülü fonksyonları bu tür sorunların çözümünü kolaylaştırmak konusunda oldukça yardımcı olur.

In [15]:
from fractions  import Fraction
Fraction.from_float(0.1)
Out[15]:
Fraction(3602879701896397, 36028797018963968)

Decimal Modülü

Decimal modülü kayan noktalı sayılarla işlemlerde hızlı ve doğru bir şekilde yuvarlama olanağı sağlayan fonksiyonlara sahiptir. Modül float nesnesine göre bazı önemli avantajlarla gelmektedir.

Decimal insanların elle işlem yapma prensiplerini temel alarak dizayn edilen bir kayan noktalı sayı modeline dayanır (IEEE Standart 854-1987). Bu şekilde bilgisayarın insanların okulda öğrendikleri şekilde işlem yapabilmelerine olanak sağlamış olur. $Decimal$ modülünde sayılar onluk sayı sisteminde temsil edildikleri şekliyle "tam olarak" temsil edilebilirler. $0.1$ gibi sayılar standart float veri türü ile gördüğünüz gibi tam olarak temsil edilememektedir. Ayrıca hem kayan noktalı (floating point) hem de sabit noktalı (fixed point) aritmetik desteği sağlar.

Modül üç kavram üzerine kuruludur: Decimal sayı nesnesi (decimal number), aritmetik işlemler ve uyarılar (Warnings). Decimal sayı nesnesi içeriği değiştirilemezdir (immutable). İşareti, mantisi ve üssü ile tanımlıdır. Sayıların sonundaki sıfırlar anlamlı rakam sayısını korumak üzere atılmaz. $Infinity$, $-Infinity$ ve $NaN$ gibi özel türleri de içerir. Aritmetik işlemler için kurallar tanımlıdır ve pek çok yuvarlama opsiyonu sağlanır (ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP ve ROUND_05UP). İşlem sonucuna göre de pek çok uyarı döndürme opsiyonu bulunmaktadır ($InvalidOperation$, $DivisionByZero$, $Inexact$, $Rounded$, $Subnormal$, $Overflow$, $Underflow$ ve $FloatOperation$). Modül, metot ve öznitelikleri için bkz..

Genel olarak onluk sayı sisteminde ifade edildiği şekilde bir metin nesnesi (string) olarak tanımlanır ve daha sonra decimal.Decimal fonksiyonu kullanılarak bir Decimal nesnesine dönüştürülür.

In [16]:
import bitstring
sayi = bitstring.BitArray(float=0.1, length=32)
print(sayi.bin)
00111101110011001100110011001101
In [17]:
from decimal import *
Decimal("0.1")
Out[17]:
Decimal('0.1')

Bir kayan noktalı sayıyı da bir Decimal dönüştürmek mümkündür. Ancak bu kez sayı amaçlandığı şekilde onluk sayı sisteminde ifade edildiği şekilde değil ikilik sayı sisteminde ifade edilebildiği şekilde onluk sayı sistemine dönüştürülmüş olur.

In [18]:
Decimal(0.1)
Out[18]:
Decimal('0.1000000000000000055511151231257827021181583404541015625')

Sayıları ondalık sayı sisteminde olduğu şekilde temsil edebildiğiniz gibi ölçüm duyarlılığı da korunur. Ancak işlemlerde anlamlı rakam sayısı sonuca tam olarak anlamlı rakamlarla aritmetik kurallarına göre transfer edilmez.

In [19]:
print(Decimal('0.1') + Decimal('0.1') + Decimal('0.1') == Decimal('0.3'))
print(Decimal('3.40') + Decimal('1.60'))
print(Decimal('0.745')*Decimal('2.2')/Decimal('3.885'))
True
5.00
0.4218790218790218790218790219

Decimal modülünde işlem yaparken sisteminiz tarafından sağlanan duyarlılığın üzerine çıkabilirsiniz. Daha fazla bilgi için bkz

In [20]:
getcontext().prec = 6
print(Decimal(1) / Decimal(7))
getcontext().prec = 28
print(Decimal(1) / Decimal(7))
Decimal('0.1428571428571428571428571429')
print(getcontext()) # limitler
0.142857
0.1428571428571428571428571429
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, FloatOperation, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])

Pek çok uygulama için $getcontext$ fonksiyonu ile mevcut ortama (kontekst) ulaşılarak ayarlar değiştirilebliir. Ancak bazen kullanıcı kendi ortamını (kontekst) yaratmaya ya da birden fazla ortamda işlem yapmaya ihtiyaç duyabilir. $Context()$, $setcontext()$ metodları yeni bir ortam oluşturmak için kullanılabilir. $decimal$ modülü tarafından sağlanan standart ortamlar (kontekstler) ise $BasicContext$ ve $ExtendedContext$'tir.

In [21]:
benimortamim = Context(prec=60, rounding=ROUND_HALF_DOWN)
setcontext(benimortamim)
print("Benim Ortamim")
print(getcontext())
print("1/7 = ", Decimal(1) / Decimal(7))
print("----------------------")
setcontext(ExtendedContext)
print("Extended Context")
print(getcontext())
print("1/7 = ", Decimal(1) / Decimal(7))
print("42 / 0 =", Decimal(42) / Decimal(0))
print("------------------------")
#print("Basic Context")
#setcontext(BasicContext)
#print("42 / 0 = ", Decimal(42) / Decimal(0))
Benim Ortamim
Context(prec=60, rounding=ROUND_HALF_DOWN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
1/7 =  0.142857142857142857142857142857142857142857142857142857142857
----------------------
Extended Context
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[])
1/7 =  0.142857143
42 / 0 = Infinity
------------------------

Kontekstlerin problemli durumlar karşısında kullanıcıyı uyarmak üzere uyarı mesajı işaretleri ($flag$) vardır. Hangi uyarı mesajlarının oluştuğunu görmek için öncelkle clear_flags() metodu ile var olan işaretleri "temizlemekte" fayda vardır.

Aşağıdaki örnekte $\pi$ sayısına yaklaşık bir oran (355 / 113) veren bir bölme işlemiyle hangi işaretlerin "uyandırıldığına" bakalım.

In [22]:
from decimal import *
setcontext(ExtendedContext)
print(getcontext())
ExtendedContext.clear_flags()
print("355 / 113 = ", Decimal("355") / Decimal("113"))
print(getcontext())
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[])
355 / 113 =  3.14159292
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[])

Burada yapılan bölmenin yaklaşık sonucunun yuvarlandığını ($Rounded$) ve bu nedenle sonucun tam olmadığını ($Inexact$) gösteren bir işaret ($Flag$) bulunmaktadır. Ayrıca getcontext() fonksiyonunun flags özniteliğiyle de hangi uyarı işaretlerinin "uyandırıldığını görebiliriz.

In [23]:
getcontext().flags
Out[23]:
{<class 'decimal.InvalidOperation'>:False, <class 'decimal.FloatOperation'>:False, <class 'decimal.DivisionByZero'>:False, <class 'decimal.Overflow'>:False, <class 'decimal.Underflow'>:False, <class 'decimal.Subnormal'>:False, <class 'decimal.Inexact'>:True, <class 'decimal.Rounded'>:True, <class 'decimal.Clamped'>:False}

Bu tür durumları belirleyerek kodumuzu ona göre değiştirmek isteyebiliriz.

In [24]:
if getcontext().flags[Inexact]:
    getcontext().prec = 64
else: 
    getcontext().prec = 28
print(getcontext().prec)
64

İstendiğinde bir kontekstin (ortamın) hata mesajı verecek şekilde ayarlanması mümkündür. Bunun için kontekstin (ortamın) traps sözlüğünden faydalanılır.

In [25]:
#setcontext(ExtendedContext)
getcontext().clear_traps()
print("1 / 0 = ", Decimal("1") / Decimal("0"))
print("Traps:")
print(getcontext().traps)
getcontext().traps[DivisionByZero] = 1
print("1 / 0 = ", Decimal("1") / Decimal("0"))
1 / 0 =  Infinity
Traps:
{<class 'decimal.InvalidOperation'>:False, <class 'decimal.FloatOperation'>:False, <class 'decimal.DivisionByZero'>:False, <class 'decimal.Overflow'>:False, <class 'decimal.Underflow'>:False, <class 'decimal.Subnormal'>:False, <class 'decimal.Inexact'>:False, <class 'decimal.Rounded'>:False, <class 'decimal.Clamped'>:False}
---------------------------------------------------------------------------
DivisionByZero                            Traceback (most recent call last)
<ipython-input-25-4f4e1081cc9c> in <module>
      5 print(getcontext().traps)
      6 getcontext().traps[DivisionByZero] = 1
----> 7 print("1 / 0 = ", Decimal("1") / Decimal("0"))

DivisionByZero: [<class 'decimal.DivisionByZero'>]

Görüldüğü gibi bir ortamın DivisionByZero işareti (flag) de ve tuzağı (trap) da bulunmaktadır. Yapılan işlemin sonucuna göre işaret $True$ değerine ayarlanırken, istendiğinde bu işarete erişilerek kodun örneğin kullanıcıya bir uyarı vermesi ya da hassasiyeti değiştirmesi sağlanabilir. Tuzak (trap) $True$ değerine ayarlandığında ise program bir hata mesajı ile hata verir. Bu noktadan sonra programcı hatayı program gerekliliklerine göre yönetir.

Örneğin kullanıcının kayan noktalı sayılar (float) yerine düzenli olarak metin (string) nesneleriyle işlem yapması istendiğinde FloatOperation tuzağı ayarlanabilir.

In [30]:
getcontext().traps[FloatOperation] = False
Decimal(2.675)
Out[30]:
Decimal('2.67499999999999982236431605997495353221893310546875')
In [27]:
Decimal("2.675")
Out[27]:
Decimal('2.675')
In [29]:
getcontext().traps[FloatOperation] = False
Decimal(0.1) + Decimal(0.1) + Decimal(0.1) == Decimal(0.3)
Out[29]:
False
In [31]:
getcontext().traps[FloatOperation] = False
Decimal("0.1") + Decimal("0.1") + Decimal("0.1") == Decimal("0.3")
Out[31]:
True

Decimal Modülünün Kullanımına Bazı Örnekler

Decimal nesnesi Python "ekosisteminin" diğer parçalarıyla uyumludur. Dolayısı ile ekosistem üzerinde çalıştırabildiğiniz pek çok fonksiyon ve işlem Decimal nesneleriyle de çalışır.

In [32]:
setcontext(BasicContext)
birliste = ['3.14','2.73', '1.80','23.7']
veri = list(map(Decimal, birliste))
veri
Out[32]:
[Decimal('3.14'), Decimal('2.73'), Decimal('1.80'), Decimal('23.7')]
In [33]:
print("Listenin maksimum degeri: ", max(veri))
print("Listenin toplami: ", sum(veri))
print("str(veri[2])", str(veri[2]))
print("veri[-1] / veri[0]", veri[-1] / veri[0])
Listenin maksimum degeri:  23.7
Listenin toplami:  31.37
str(veri[2]) 1.80
veri[-1] / veri[0] 7.54777070

Decimal nesnelerine matematiksel fonksiyonlar uygulanabildiği gibi bu fonksiyonlar için metotlar da bulunmaktadır.

In [34]:
from math import e,sin,log10
print(sin(Decimal('3.14159')))
print(log10(Decimal('1000')))
print(Decimal('1000').log10())
print(Decimal(e).ln())
2.65358979335273e-06
3.0
3
1.00000000

Decimal nesnelerle aritmetik işlemler ile tamsayılar ve kayan noktalı sayılarla yapılan aritmetik işlemler arasında bazı küçük farklar vardır. Decimal nesnelere % kalan operatörü uygulandığında, sonucun işareti bölenin işareti değil, bölünenin işaretidir. Bu iki işlemin aslında farklı bir mantıkta çalıştığını göstermektedir. Birinci işlem bir modüler aritmetik işlemi ilken ikincisi bir negatif bölme kalanı olarak düşünülmelidir.

In [35]:
print(-7 % 4)
1
In [36]:
print(Decimal("-7") % Decimal("4"))
-3

Tamsayı bölme operatörü de // benzer şekilde davranır, $x == (x // y) * y + x \% y$ eşitiğini korumak için gerçek bölümün tabanı (aşağı yuvarlanması) yerine (sıfıra doğru kesilerek) tamsayı kısmı döndürülür.

In [37]:
print(-7 // 4)
-2
In [38]:
print(Decimal(-7) // Decimal(4))
-1

Decimal Modülünde Yuvarlama Seçenekleri

Decimal modülünde yuvarlama işlemleri için quantize metodu kullanılır. Herhangi bir yuvarlama stratejisi kullanılabilir ancak varsayılan yöntemde (ROUND_HALF_EVEN) yuvarlamada tıpkı round fonksiyonunda olduğu gibi, klasik olarak takip edilen yuvarlama kurallarına uyulur. Ancak bu kez yuvarlamada sayının gösterim şekli esas alınır.

In [39]:
print(Decimal('1.65').quantize(Decimal('1.0')))
print(Decimal('7.325').quantize(Decimal('.01'), rounding=ROUND_DOWN))
print(Decimal('7.325').quantize(Decimal('1.'), rounding=ROUND_UP))
1.7
7.32
8

Son olarak kullanılan yuvarlama kuralı kalıcı hale getirildiğinden varsayılana dönmek için varsayılan kuralın tekrar hatırlatılması gerekir.

In [40]:
print(Decimal('1.65').quantize(Decimal('1.0')))
print(Decimal('1.65').quantize(Decimal('1.0'), rounding=ROUND_HALF_EVEN))
print(Decimal("2.675").quantize(Decimal("1.00")))
1.7
1.6
2.68

Decimal modülünün yuvarlama fonksiyonlarından bazıları klasik yuvarlama stratejilerinden bir miktar farklı çalışmaktadır. Örneğin ROUND_HALF_UP fonksiyonu beklentinin (ve genel kuralın) aksine sayıları 0'dan uzaklatırşacak şekilde yuvarlarken ROUND_HALF_DOWN sayıları 0'a yaklaştıracak şekilde yuvarlar. Bu yaklaşımın temel amacı yuvarlama yanlılığını minimize etmektir.

In [41]:
getcontext().rounding = ROUND_UP
print(Decimal("4.42").quantize(Decimal("1.0")))
print(Decimal("-4.42").quantize(Decimal("1.0")))
getcontext().rounding = ROUND_DOWN
print(Decimal("6.28").quantize(Decimal("1.0")))
print(Decimal("-6.28").quantize(Decimal("1.0")))
4.5
-4.5
6.2
-6.2

Bu iki fonksiyonun sondaki rakamın 5 olması durumundaki her iki yöndeki (pozitif ve negatif) davranışları da aynı şekildedir. ROUND_UP 0'dan uzaklaştıracak, ROUND_DOWN 0'a yaklaştıracak şekilde sayıları yuvarlar. ROUND_HALF_UP ve ROUND_HALF_DOWN 'ın davranışları da benzerdir.

In [42]:
getcontext().rounding = ROUND_HALF_UP
print(Decimal("4.45").quantize(Decimal("1.0")))
print(Decimal("-4.45").quantize(Decimal("1.0")))
getcontext().rounding = ROUND_HALF_DOWN
print(Decimal("6.25").quantize(Decimal("1.0")))
print(Decimal("-6.25").quantize(Decimal("1.0")))
4.5
-4.5
6.2
-6.2

Decimal modülündeki ROUND_05UP fonksiyonu ise ilginç bir yuvarlama stratejisi daha sağlar.

In [43]:
getcontext().rounding = ROUND_05UP
print(Decimal("3.76").quantize(Decimal("1.0")))
print(Decimal("4.45").quantize(Decimal("1.0")))
print(Decimal("-4.45").quantize(Decimal("1.0")))
3.7
4.4
-4.4

Yukarıdaki davranışı itibarı ile strateji her sayıyı 0'a doğru yuvarlar gibi görünmektedir. Ancak aşağıdaki ilk örnekte sayı önce 0'a doğru yuvarlanmaktadır ($1.4$). Ancak bir sonraki sayı 4 olduğu (0 ya da 5 olmadığı) için sayı olduğu gibi bırakılmaktadır. İkinci örnekte ise sayı 0' doğru yuvarlandıktan sonra ($1.5$) 5 ile karşılaşılmakta, bu ise sayının yukarıya yuvarlanmasına neden olmaktadır. Karşılaşılan sayı 0 olduğunda da davranış şekli aynıdır.

In [44]:
print(Decimal("1.49").quantize(Decimal("1.0")))
print(Decimal("1.51").quantize(Decimal("1.0")))
1.4
1.6

Numpy Modülünde Yuvarlama Seçenekleri

Numpy modülündeki around fonksiyonu, Python'un round fonksiyonuna benzer şekilde çalışır ve tıpkı onun gibi sayıların mükemmel temsil edilememesi sorunundan muzdariptir.

In [45]:
import numpy as np
np.random.seed(444) #her calismada ayni ciktiyi uretmek icin
dizi = np.random.randn(3, 5)
print(dizi)
yeni_dizi = np.around(dizi, decimals=4)
print(yeni_dizi)
[[ 0.35743992  0.3775384   1.38233789  1.17554883 -0.9392757 ]
 [-1.14315015 -0.54243951 -0.54870808  0.20851975  0.21268956]
 [ 1.26802054 -0.80730293 -3.30307164 -0.80664959 -0.36032936]]
[[ 0.3574  0.3775  1.3823  1.1755 -0.9393]
 [-1.1432 -0.5424 -0.5487  0.2085  0.2127]
 [ 1.268  -0.8073 -3.3031 -0.8066 -0.3603]]

math modülündeki ceil, trunc ve floor fonksiyonlarının diziler için çalışan eşlenikleri numpy için de vardır.

In [46]:
print(np.ceil(dizi))
print(np.trunc(dizi))
print(np.floor(dizi))
[[ 1.  1.  2.  2. -0.]
 [-1. -0. -0.  1.  1.]
 [ 2. -0. -3. -0. -0.]]
[[ 0.  0.  1.  1. -0.]
 [-1. -0. -0.  0.  0.]
 [ 1. -0. -3. -0. -0.]]
[[ 0.  0.  1.  1. -1.]
 [-2. -1. -1.  0.  0.]
 [ 1. -1. -4. -1. -1.]]

Sayılardan bazılarının negatif sıfıra ($-0$) yuvarlandığında dikkat ediniz. Negatif 0, bize bu sayının yuvarlandığını ve 0'dan küçük bir sayıdan yuvarlandığını söylemektedir. Bu durum, örneğin suyun sıcaklığı olduğunda onun donma noktasnın altında bir sıcaklığa sahip olduğu konusunda bir fikir verebilir.

Tam sayıya yuvarlamada klasik yuvarlama stratejisi numpy modülünde rint fonksiyonu ile sağlanmıştır.

In [47]:
print(np.rint(dizi))
[[ 0.  0.  1.  1. -1.]
 [-1. -1. -1.  0.  0.]
 [ 1. -1. -3. -1. -0.]]

Diğer yuvarlama seçenekleri numpy'da verilen bu seçenekler kullanılarak kodlanabilir. Numpy modülü Decimal modülü ile birlikte kullanılamamaktadır. Yine de istenirse, istenen gösterimdeki metin nesneleri bir liste içinde Decimal nesnesine dönüştürüldükten sonra numpy.array fonksiyonu ile numpy dizilerine dönüştürülebilir.

Pandas Modülünde Yuvarlama Seçenekleri

math modülündeki round ve numpy modülündeki around fonksiyonlarının pandas modülündeki karşılığı da round fonksiyonudur ve hem pandas serileri (pandas.series) hem de veri çeerçeveleri (pandas.DataFrame) üzerinde kullanılabilir.

In [48]:
import pandas as pd
import numpy as np
np.random.seed(444)
seri = pd.Series(np.random.randn(5))
print(seri)
0    0.357440
1    0.377538
2    1.382338
3    1.175549
4   -0.939276
dtype: float64
In [49]:
print(seri.round(3))
0    0.357
1    0.378
2    1.382
3    1.176
4   -0.939
dtype: float64
In [50]:
np.random.seed(1234)
df = pd.DataFrame(np.random.randn(4, 4), columns=["A", "B", "C", "D"])
print(df)
df.round(2)
          A         B         C         D
0  0.471435 -1.190976  1.432707 -0.312652
1 -0.720589  0.887163  0.859588 -0.636524
2  0.015696 -2.242685  1.150036  0.991946
3  0.953324 -2.021255 -0.334077  0.002118
Out[50]:
A B C D
0 0.47 -1.19 1.43 -0.31
1 -0.72 0.89 0.86 -0.64
2 0.02 -2.24 1.15 0.99
3 0.95 -2.02 -0.33 0.00

İstenirse her bir sütun (seriye) ayrı bir basamağa da yuvarlanabilir.

In [51]:
df.round({"A": 1, "B": 2, "C": 3, "D": 0})
Out[51]:
A B C D
0 0.5 -1.19 1.433 -0.0
1 -0.7 0.89 0.860 -1.0
2 0.0 -2.24 1.150 1.0
3 1.0 -2.02 -0.334 0.0
In [52]:
basamak_sayisi = pd.Series([3, 0, 1, 2], index=["A", "B", "C", "D"])
df.round(basamak_sayisi)
Out[52]:
A B C D
0 0.471 -1.0 1.4 -0.31
1 -0.721 1.0 0.9 -0.64
2 0.016 -2.0 1.2 0.99
3 0.953 -2.0 -0.3 0.00

numpy modülünün ceil, trunc, floor ve rint fonksiyonları pandas serileri ve dizileri üzerinde de kullanılabilir.

In [53]:
print(np.ceil(dizi))
print(np.floor(dizi))
print(np.trunc(dizi))
print(np.rint(dizi))
[[ 1.  1.  2.  2. -0.]
 [-1. -0. -0.  1.  1.]
 [ 2. -0. -3. -0. -0.]]
[[ 0.  0.  1.  1. -1.]
 [-2. -1. -1.  0.  0.]
 [ 1. -1. -4. -1. -1.]]
[[ 0.  0.  1.  1. -0.]
 [-1. -0. -0.  0.  0.]
 [ 1. -0. -3. -0. -0.]]
[[ 0.  0.  1.  1. -1.]
 [-1. -1. -1.  0.  0.]
 [ 1. -1. -3. -1. -0.]]

Gerek yuvarlamada, gerekse hata hesaplarında kritik olan tüm kavramalara hakim olup yerli yerince kullanmaktır. Kullanılan gözlemsel / deneysel verinin duyarlılığına bağlı seçimler yapılarak uygun yuvarlama / kesme seçenekleri kullanılmalı; anlamlı rakamlara özen gösterilmelidir. İşlemler ve fonksiyonlar sonucu elde edilen niceliklerin duyarlılıkları (üzerlerindeki belirsizlikler) verilirken işleme giren parametrelerin üzerindeki belirsizlikler dikkate alınmalıdır. Bir sonucun hatası verileriken hatanın türü (standart sapma, standart hata, tahmini hata, uyumlamadan fark karelerin toplamının karekökü (ing. root-mean-square, rms), varyans ...) mutlaka belirtilmeli, verilen niceliğin basamak sayısı ile hatasının basamak sayısnın aynı olmasına özen gösterilmeldir.

Başa Dön

Kaynaklar