Memotong pelampung dengan Python

110

Saya ingin menghapus digit dari float agar memiliki jumlah digit tetap setelah titik, seperti:

1.923328437452 -> 1.923

Saya perlu mengeluarkan sebagai string ke fungsi lain, bukan mencetak.

Juga saya ingin mengabaikan digit yang hilang, bukan membulatkannya.

Joan Venge
sumber
4
Haruskah -1.233 dipotong menjadi -1.23 atau -1.24?
Antony Hatchkins

Jawaban:

117

Pertama, fungsinya, bagi mereka yang hanya menginginkan kode salin dan tempel:

def truncate(f, n):
    '''Truncates/pads a float f to n decimal places without rounding'''
    s = '{}'.format(f)
    if 'e' in s or 'E' in s:
        return '{0:.{1}f}'.format(f, n)
    i, p, d = s.partition('.')
    return '.'.join([i, (d+'0'*n)[:n]])

Ini berlaku di Python 2.7 dan 3.1+. Untuk versi yang lebih lama, tidak mungkin untuk mendapatkan efek "pembulatan cerdas" yang sama (setidaknya, bukan tanpa banyak kode yang rumit), tetapi pembulatan ke 12 tempat desimal sebelum pemotongan akan sering dilakukan:

def truncate(f, n):
    '''Truncates/pads a float f to n decimal places without rounding'''
    s = '%.12f' % f
    i, p, d = s.partition('.')
    return '.'.join([i, (d+'0'*n)[:n]])

Penjelasan

Inti dari metode yang mendasari adalah mengubah nilai menjadi string dengan presisi penuh dan kemudian memotong semuanya di luar jumlah karakter yang diinginkan. Langkah terakhir ini mudah; itu bisa dilakukan baik dengan manipulasi string

i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])

atau decimalmodul

str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))

Langkah pertama, mengubah ke string, cukup sulit karena ada beberapa pasangan literal floating point (yaitu apa yang Anda tulis di kode sumber) yang keduanya menghasilkan representasi biner yang sama namun harus dipotong secara berbeda. Misalnya, pertimbangkan 0,3 dan 0,29999999999999998. Jika Anda menulis 0.3dalam program Python, kompilator mengkodekannya menggunakan format titik-mengambang IEEE ke dalam urutan bit (dengan asumsi float 64-bit)

0011111111010011001100110011001100110011001100110011001100110011

Ini adalah nilai terdekat dengan 0,3 yang secara akurat dapat direpresentasikan sebagai float IEEE. Tetapi jika Anda menulis 0.29999999999999998dalam program Python, kompilator menerjemahkannya menjadi nilai yang persis sama . Dalam satu kasus, Anda bermaksud memangkasnya (menjadi satu digit) sebagai 0.3, sedangkan dalam kasus lain Anda bermaksud memangkasnya 0.2, tetapi Python hanya dapat memberikan satu jawaban. Ini adalah batasan mendasar Python, atau memang bahasa pemrograman apa pun tanpa evaluasi malas. Fungsi pemotongan hanya memiliki akses ke nilai biner yang disimpan di memori komputer, bukan string yang sebenarnya Anda ketikkan ke dalam kode sumber. 1

Jika Anda mendekode urutan bit kembali menjadi angka desimal, sekali lagi menggunakan format titik mengambang IEEE 64-bit, Anda mendapatkan

0.2999999999999999888977697537484345957637...

jadi implementasi yang naif akan muncul 0.2meskipun itu mungkin bukan yang Anda inginkan. Untuk lebih lanjut tentang kesalahan representasi floating-point, lihat tutorial Python .

Sangat jarang bekerja dengan nilai floating-point yang sangat dekat dengan bilangan bulat tetapi sengaja tidak sama dengan bilangan bulat itu. Jadi, saat memotong, mungkin masuk akal untuk memilih representasi desimal "terbaik" dari semua yang dapat sesuai dengan nilai dalam memori. Python 2.7 dan yang lebih baru (tetapi bukan 3.0) menyertakan algoritme canggih untuk melakukan hal itu , yang dapat kita akses melalui operasi pemformatan string default.

'{}'.format(f)

Satu-satunya peringatan adalah bahwa ini bertindak seperti gspesifikasi format, dalam arti bahwa ia menggunakan notasi eksponensial ( 1.23e+4) jika angkanya cukup besar atau kecil. Jadi metode ini harus menangkap kasus ini dan menanganinya secara berbeda. Ada beberapa kasus di mana menggunakan fspesifikasi format malah menyebabkan masalah, seperti mencoba memotong 3e-10ke presisi 28 digit (menghasilkan 0.0000000002999999999999999980), dan saya belum yakin cara terbaik untuk menanganinya.

Jika Anda benar - benar bekerja dengan floats yang sangat dekat dengan bilangan bulat tetapi sengaja tidak sama dengan bilangan tersebut (seperti 0.29999999999999998 atau 99.959999999999994), ini akan menghasilkan beberapa positif palsu, yaitu bilangan bulat yang tidak ingin dibulatkan. Dalam hal ini, solusinya adalah dengan menentukan presisi tetap.

'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)

Jumlah digit ketepatan yang digunakan di sini tidak terlalu penting, hanya perlu cukup besar untuk memastikan bahwa pembulatan apa pun yang dilakukan dalam konversi string tidak "menaikkan" nilai ke representasi desimalnya yang bagus. Saya pikir sys.float_info.dig + n + 2mungkin cukup dalam semua kasus, tetapi jika tidak 2mungkin harus ditingkatkan, dan tidak ada salahnya untuk melakukannya.

Pada versi Python sebelumnya (hingga 2.6, atau 3.0), pemformatan angka floating point jauh lebih kasar, dan secara teratur akan menghasilkan hal-hal seperti

>>> 1.1
1.1000000000000001

Jika ini adalah situasi Anda, jika Anda tidak ingin menggunakan "bagus" representasi desimal untuk pemotongan, semua dapat Anda lakukan (sejauh yang saya tahu) adalah memilih beberapa jumlah digit, kurang dari representable presisi penuh oleh float, dan putaran nomor sebanyak itu sebelum memotongnya. Pilihan tipikal adalah 12,

'%.12f' % f

tetapi Anda dapat menyesuaikan ini agar sesuai dengan angka yang Anda gunakan.


1 Yah ... aku berbohong. Secara teknis, Anda dapat menginstruksikan Python untuk mengurai ulang kode sumbernya sendiri dan mengekstrak bagian yang sesuai dengan argumen pertama yang Anda berikan ke fungsi pemotongan. Jika argumen tersebut adalah literal floating-point, Anda bisa memotongnya di sejumlah tempat setelah koma desimal dan mengembalikannya. Namun strategi ini tidak berhasil jika argumennya adalah variabel, yang membuatnya tidak berguna. Hal berikut disajikan hanya untuk hiburan:

def trunc_introspect(f, n):
    '''Truncates/pads the float f to n decimal places by looking at the caller's source code'''
    current_frame = None
    caller_frame = None
    s = inspect.stack()
    try:
        current_frame = s[0]
        caller_frame = s[1]
        gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline)
        for token_type, token_string, _, _, _ in gen:
            if token_type == tokenize.NAME and token_string == current_frame[3]:
                next(gen) # left parenthesis
                token_type, token_string, _, _, _ = next(gen) # float literal
                if token_type == tokenize.NUMBER:
                    try:
                        cut_point = token_string.index('.') + n + 1
                    except ValueError: # no decimal in string
                        return token_string + '.' + '0' * n
                    else:
                        if len(token_string) < cut_point:
                            token_string += '0' * (cut_point - len(token_string))
                        return token_string[:cut_point]
                else:
                    raise ValueError('Unable to find floating-point literal (this probably means you called {} with a variable)'.format(current_frame[3]))
                break
    finally:
        del s, current_frame, caller_frame

Menggeneralisasi ini untuk menangani kasus di mana Anda meneruskan variabel tampaknya seperti penyebab yang hilang, karena Anda harus menelusuri mundur melalui eksekusi program sampai Anda menemukan literal floating-point yang memberi variabel nilainya. Bahkan jika ada. Sebagian besar variabel akan diinisialisasi dari input pengguna atau ekspresi matematika, dalam hal ini representasi biner adalah segalanya.

David Z
sumber
Bagaimana kita bisa menerapkan fungsi ini ke dataframe?
kode tuan
@RohithRNair Di atas kepala saya, cara yang sama Anda menerapkan fungsi lain yang beroperasi pada elemen individu (yaitu applymap()). Mungkin ada cara untuk membuat seluruh operasi lebih efisien, tapi itu akan menjadi masalah untuk pertanyaan terpisah.
David Z
applymap () memakan banyak waktu karena dataframe saya sangat besar. Saya mencoba membandingkan dua dataframe untuk perbedaan tetapi presisi floating point membelokkan keluaran saya dari yang diinginkan. Seperti yang Anda katakan, saya akan mengajukan pertanyaan terpisah untuk hal yang sama. Terima kasih.
kode tuan
@RohithRNair Ah, jika Anda mencoba membandingkan dua kerangka data untuk mengetahui perbedaan, tanyakan tentang itu. Memotong nilai (tentang pertanyaan ini) bukanlah cara terbaik untuk melakukannya.
David Z
Sekadar catatan, kode Anda tampaknya memotong angka negatif menjadi nol negatif, yang bisa membingungkan ...
pengguna541686
152
round(1.923328437452, 3)

Lihat dokumentasi Python tentang tipe standar . Anda harus menggulir sedikit ke bawah untuk mendapatkan fungsi bulat. Pada dasarnya angka kedua menunjukkan berapa banyak tempat desimal untuk membulatkannya.

Teifion
sumber
49
Maksud saya pembulatan bukanlah yang saya butuhkan. Saya perlu pemotongan, yang berbeda.
Joan Venge
1
Ahhh, cukup adil. Kesalahan saya, maaf.
Teifion
22
Itu banyak suara positif untuk solusi yang salah! Salah satu kelangkaan Stackoverflow yang aneh. Saya ingin tahu apakah ada lencana untuk itu ...
tumultous_rooster
5
Sungguh mengerikan betapa banyak jawaban yang salah (dan suara positif untuk jawaban yang salah) ada untuk pertanyaan ini.
nullstellatz
6
Banyak orang akan datang ke halaman ini untuk mencari pembulatan;)
janjackson
33

Hasilnya roundadalah float, jadi hati-hati (contohnya dari Python 2.6):

>>> round(1.923328437452, 3)
1.923
>>> round(1.23456, 3)
1.2350000000000001

Anda akan lebih baik jika menggunakan string yang diformat:

>>> "%.3f" % 1.923328437452
'1.923'
>>> "%.3f" % 1.23456
'1.235'
Ferdinand Beyer
sumber
8
Pada Python saya, putaran itu: '% .3f'% 1.23456 == '1.235'
David Z
Ini jauh lebih elegan daripada format string manual yang tidak masuk akal, posting bagus!
rsethc
round(1.23456, 3)adalah 1.235dan tidak1.2350000000000001
Ahmad
1
@Ahmad belum tentu. Contoh di sini adalah dari Python 2.6 (perhatikan tanggal jawabannya). Pemformatan string ditingkatkan dengan Python 2.7 / 3.1, itu mungkin mengapa Anda mendapatkan hasil yang berbeda. Namun demikian, bilangan floating point sering kali memiliki representasi string yang tidak terduga, lihat: docs.python.org/3.6/tutorial/floatingpoint.html
Ferdinand Beyer
21
n = 1.923328437452
str(n)[:4]
john_dough
sumber
3
Sederhana dan pythonic. 4 adalah ukuran bilangan bulat, tidak hanya angka setelah titik.
GaTTaCa
4
Jadi jika pengguna memasukkan misalnya 2, Anda akan memiliki titik desimal .di akhir string - menurut saya bukan solusi yang baik.
Zelphir Kaltstahl
Ini khusus untuk kasus nomor ini. Bagaimana cara menggeneralisasi menjadi 11.923328437452?
mempolarisasikan
Jawaban Terbaik! Anda juga bisa menambahkan float () untuk mengembalikan angka: float (str (n) [: 4])
justSaid
14

Pada prompt Python 2.7 saya:

>>> int(1.923328437452 * 1000)/1000.0 1.923

Tagihan
sumber
11

Skrip python sederhana -

n = 1.923328437452
n = float(int(n * 1000))
n /=1000
markroxor
sumber
3
Jawaban yang bersih. Anda hanya melewatkan satu langkah, untuk mengubahnya kembali menjadi float sebelum membaginya dengan 1000. Jika tidak, Anda akan mendapatkan 1.
Yohan Obadia
9
def trunc(num, digits):
   sp = str(num).split('.')
   return '.'.join([sp[0], sp[1][:digits]])

Ini seharusnya berhasil. Ini harus memberi Anda pemotongan yang Anda cari.

Matt
sumber
9

Cara yang benar-benar pythonic untuk melakukannya adalah

from decimal import *

with localcontext() as ctx:
    ctx.rounding = ROUND_DOWN
    print Decimal('1.923328437452').quantize(Decimal('0.001'))

atau lebih pendek:

from decimal import Decimal as D, ROUND_DOWN

D('1.923328437452').quantize(D('0.001'), rounding=ROUND_DOWN)

Memperbarui

Biasanya masalahnya bukan pada pemotongan float itu sendiri, tetapi pada penggunaan nomor float yang tidak tepat sebelumnya pembulatan.

Sebagai contoh: int(0.7*3*100)/100 == 2.09 .

Jika Anda dipaksa untuk menggunakan float (katakanlah, Anda mempercepat kode Anda dengan numba), lebih baik menggunakan sen sebagai "representasi internal" dari harga: ( 70*3 == 210) dan mengalikan / membagi input / output.

Antony Hatchkins
sumber
Maafkan saya karena menanyakan hal ini, tapi ... mengapa?
markroxor
@markroxor, tidak yakin apa yang sebenarnya Anda tanyakan. Sebagai catatan samping, biasanya masalahnya bukan pada pembulatan itu sendiri, tetapi dengan penggunaan bilangan pelampung yang tidak tepat sebelum pembulatan. Mis int(0.7*3*100)/100 == 2.09. Kemana perginya 1 sen saya?
Antony Hatchkins
masuk akal, bisakah Anda mengedit jawaban Anda dengan penjelasan ini? Terima kasih.
markroxor
Mendapatkan ImportError: cannot import name 'D', saya yakin Anda ingin membuat import bernama no?
Overdrivr
8

Begitu banyak jawaban yang diberikan untuk pertanyaan ini benar-benar salah. Mereka membulatkan pelampung (bukan memotong) atau tidak bekerja untuk semua kasus.

Ini adalah hasil teratas Google ketika saya mencari 'Python truncate float', sebuah konsep yang sangat mudah, dan yang pantas mendapatkan jawaban yang lebih baik. Saya setuju dengan Hatchkins bahwa menggunakan decimalmodul adalah cara pythonic untuk melakukan ini, jadi saya berikan di sini fungsi yang menurut saya menjawab pertanyaan dengan benar, dan yang berfungsi seperti yang diharapkan untuk semua kasus.

Sebagai catatan samping, nilai pecahan, secara umum, tidak dapat direpresentasikan secara tepat oleh variabel floating point biner (lihat di sini untuk pembahasannya), itulah mengapa fungsi saya mengembalikan string.

from decimal import Decimal, localcontext, ROUND_DOWN

def truncate(number, places):
    if not isinstance(places, int):
        raise ValueError("Decimal places must be an integer.")
    if places < 1:
        raise ValueError("Decimal places must be at least 1.")
    # If you want to truncate to 0 decimal places, just do int(number).

    with localcontext() as context:
        context.rounding = ROUND_DOWN
        exponent = Decimal(str(10 ** - places))
        return Decimal(str(number)).quantize(exponent).to_eng_string()
nullstellensatz
sumber
4

Saya melakukan sesuatu seperti ini:

from math import trunc


def truncate(number, decimals=0):
    if decimals < 0:
        raise ValueError('truncate received an invalid value of decimals ({})'.format(decimals))
    elif decimals == 0:
        return trunc(number)
    else:
        factor = float(10**decimals)
        return trunc(number*factor)/factor
Alvaro
sumber
4

Anda dapat melakukan:

def truncate(f, n):
    return math.floor(f * 10 ** n) / 10 ** n

pengujian:

>>> f=1.923328437452
>>> [truncate(f, n) for n in range(5)]
[1.0, 1.9, 1.92, 1.923, 1.9233]

sumber
Ini hanya memotong dengan angka positif, angka negatif akan membulatkan ke bawah (menjauh dari nol).
Aaron D
3

Jika Anda menyukai sihir matematika, ini berfungsi untuk + lima angka:

>>> v = 1.923328437452
>>> v - v % 1e-3
1.923
cs95
sumber
Seperti yang saya pahami, 1e-3 akan dipotong menjadi 3 digit setelah titik. Saya menyukai jawaban ini tetapi tampaknya tidak berfungsi untuk 4 dan 5.
egvo
2

Saat menggunakan pandas df ini berhasil untuk saya

import math
def truncate(number, digits) -> float:
    stepper = 10.0 ** digits
    return math.trunc(stepper * number) / stepper

df['trunc'] = df['float_val'].apply(lambda x: truncate(x,1))
df['trunc']=df['trunc'].map('{:.1f}'.format)
bart cubrich
sumber
1

Hanya ingin menyebutkan bahwa trik lama "make round () with floor ()"

round(f) = floor(f+0.5)

dapat diputar untuk membuat floor () dari round ()

floor(f) = round(f-0.5)

Meskipun kedua aturan ini melanggar bilangan negatif, jadi menggunakannya kurang dari ideal:

def trunc(f, n):
    if f > 0:
        return "%.*f" % (n, (f - 0.5*10**-n))
    elif f == 0:
        return "%.*f" % (n, f)
    elif f < 0:
        return "%.*f" % (n, (f + 0.5*10**-n))
itsadok
sumber
1

int (16.5); ini akan memberikan nilai integer 16, yaitu trunc, tidak akan bisa menentukan desimal, tapi tebak Anda bisa melakukannya dengan

import math;

def trunc(invalue, digits):
    return int(invalue*math.pow(10,digits))/math.pow(10,digits);
Pieter
sumber
1

Berikut cara mudahnya:

def truncate(num, res=3):
    return (floor(num*pow(10, res)+0.5))/pow(10, res)

untuk num = 1.923328437452, ini menghasilkan 1.923

Sarang
sumber
1
def trunc(f,n):
  return ('%.16f' % f)[:(n-16)]
Ross Cartlidge
sumber
1

Fungsi umum dan sederhana untuk digunakan:

def truncate_float(number, length):
    """Truncate float numbers, up to the number specified
    in length that must be an integer"""

    number = number * pow(10, length)
    number = int(number)
    number = float(number)
    number /= pow(10, length)
    return number
Yohan Obadia
sumber
Bagus! Pemeran ke int memotong bilangan positif dan negatif.
Aaron D
1

Ada solusi mudah di python 3. Di mana untuk memotong saya mendefinisikan dengan decPlace variabel bantuan untuk membuatnya mudah beradaptasi.

f = 1.12345
decPlace= 4
f_cut = int(f * 10**decPlace) /10**decPlace

Keluaran:

f = 1.1234

Semoga membantu.

MBreg
sumber
1
def precision(value, precision):
    """
    param: value: takes a float
    param: precision: int, number of decimal places
    returns a float
    """
    x = 10.0**precision
    num = int(value * x)/ x
    return num
precision(1.923328437452, 3)

1.923

Andrew Olson
sumber
Bagus tapi Anda tidak membulatkan.
Alex
1

Varian pendek dan mudah

def truncate_float(value, digits_after_point=2):
    pow_10 = 10 ** digits_after_point
    return (float(int(value * pow_10))) / pow_10

>>> truncate_float(1.14333, 2)
>>> 1.14

>>> truncate_float(1.14777, 2)
>>> 1.14


>>> truncate_float(1.14777, 4)
>>> 1.1477
megajoe
sumber
1

Sebagian besar jawaban terlalu rumit menurut saya, bagaimana dengan ini?

digits = 2  # Specify how many digits you want

fnum = '122.485221'
truncated_float = float(fnum[:fnum.find('.') + digits + 1])

>>> 122.48

Cukup memindai indeks '.' dan potong sesuai keinginan (tanpa pembulatan). Ubah string menjadi float sebagai langkah terakhir.

Atau dalam kasus Anda jika Anda mendapatkan float sebagai input dan menginginkan string sebagai output:

fnum = str(122.485221)  # convert float to string first
truncated_float = fnum[:fnum.find('.') + digits + 1]  # string output
H123321
sumber
Proposal Anda bermasalah jika angka yang dipotong kecil karena Anda akan membuang banyak ketepatan dengan 0 di depan kanan koma desimal. Tapi masalah ini endemik dari masalah yang disebutkan. Yang ingin saya katakan adalah bahwa angka penting adalah jawaban sebenarnya.
overcoil
1
>>> floor((1.23658945) * 10**4) / 10**4
1.2365

# bagi dan kalikan dengan 10 ** jumlah digit yang diinginkan

JohnA
sumber
0

gunakan numpy.round

import numpy as np
precision = 3
floats = [1.123123123, 2.321321321321]
new_float = np.round(floats, precision)
rafaelvalle
sumber
0

Sesuatu yang cukup sederhana agar sesuai dengan pemahaman daftar, tanpa perpustakaan atau dependensi eksternal lainnya. Untuk Python> = 3.6, menulis dengan f-string sangat mudah.

Idenya adalah membiarkan konversi string melakukan pembulatan ke satu tempat lebih banyak dari yang Anda butuhkan dan kemudian memotong digit terakhir.

>>> nout = 3  # desired number of digits in output
>>> [f'{x:.{nout+1}f}'[:-1] for x in [2/3, 4/5, 8/9, 9/8, 5/4, 3/2]]
['0.666', '0.800', '0.888', '1.125', '1.250', '1.500']

Tentu saja, ada yang pembulatan terjadi di sini (yaitu untuk digit keempat), tetapi pembulatan di beberapa titik adalah unvoidable. Jika transisi antara pemotongan dan pembulatan relevan, berikut adalah contoh yang sedikit lebih baik:

>>> nacc = 6  # desired accuracy (maximum 15!)
>>> nout = 3  # desired number of digits in output
>>> [f'{x:.{nacc}f}'[:-(nacc-nout)] for x in [2.9999, 2.99999, 2.999999, 2.9999999]]
>>> ['2.999', '2.999', '2.999', '3.000']

Bonus: menghilangkan angka nol di sebelah kanan

>>> nout = 3  # desired number of digits in output
>>> [f'{x:.{nout+1}f}'[:-1].rstrip('0') for x in [2/3, 4/5, 8/9, 9/8, 5/4, 3/2]]
['0.666', '0.8', '0.888', '1.125', '1.25', '1.5']
Axel
sumber
0

Ide inti yang diberikan di sini menurut saya merupakan pendekatan terbaik untuk masalah ini. Sayangnya, ia menerima lebih sedikit suara sementara jawaban selanjutnya yang memiliki lebih banyak suara tidak lengkap (seperti yang diamati di komentar). Semoga penerapan di bawah ini memberikan solusi singkat dan lengkap untuk pemotongan .

def trunc(num, digits):
    l = str(float(num)).split('.')
    digits = min(len(l[1]), digits)
    return (l[0]+'.'+l[1][:digits])

yang seharusnya menangani semua kasus sudut yang ditemukan di sini dan di sini .

aak318
sumber
-1

Saya juga pemula python dan setelah memanfaatkan beberapa bagian di sini, saya menawarkan dua sen saya

print str(int(time.time()))+str(datetime.now().microsecond)[:3]

str (int (time.time ())) akan mengambil waktu epoch sebagai int dan mengubahnya menjadi string dan bergabung dengan ... str (datetime.now (). microsecond) [: 3] yang mengembalikan mikrodetik saja, konversikan untuk merangkai dan memotong ke 3 karakter pertama

pengguna1048839
sumber
-1
# value  value to be truncated
# n  number of values after decimal

value = 0.999782
n = 3
float(int(value*1en))*1e-n
Praveen Ramanujam
sumber
-3

Jika yang Anda maksud saat mencetak, maka berikut ini seharusnya berfungsi:

print '%.3f' % number
pengguna44511
sumber
2
Itu membulatkan angka, itu tidak memotong.
David Z