Helaian notepad-cheat untuk prapemprosesan Data yang pantas

Selalunya orang yang memasuki bidang Sains Data mempunyai jangkaan yang kurang realistik tentang apa yang menanti mereka. Ramai orang berfikir bahawa kini mereka akan menulis rangkaian saraf yang hebat, mencipta pembantu suara daripada Iron Man, atau mengalahkan semua orang dalam pasaran kewangan.
Tetapi kerja Tarikh Saintis dipacu data, dan salah satu aspek yang paling penting dan memakan masa ialah memproses data sebelum memasukkannya ke dalam rangkaian saraf atau menganalisisnya dengan cara tertentu.

Dalam artikel ini, pasukan kami akan menerangkan cara anda boleh memproses data dengan cepat dan mudah dengan arahan dan kod langkah demi langkah. Kami cuba menjadikan kod itu agak fleksibel dan boleh digunakan untuk set data yang berbeza.

Ramai profesional mungkin tidak menemui apa-apa yang luar biasa dalam artikel ini, tetapi pemula akan dapat mempelajari sesuatu yang baru, dan sesiapa yang telah lama bermimpi untuk membuat buku nota berasingan untuk pemprosesan data yang pantas dan berstruktur boleh menyalin kod dan memformatnya untuk diri mereka sendiri, atau muat turun buku nota siap dari Github.

Kami menerima set data. Apa yang perlu dilakukan seterusnya?

Jadi, standardnya: kita perlu memahami apa yang kita hadapi, gambaran keseluruhan. Untuk melakukan ini, kami menggunakan panda untuk mentakrifkan jenis data yang berbeza.

import pandas as pd #ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ pandas
import numpy as np  #ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ numpy
df = pd.read_csv("AB_NYC_2019.csv") #Ρ‡ΠΈΡ‚Π°Π΅ΠΌ датасСт ΠΈ записываСм Π² ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ df

df.head(3) #смотрим Π½Π° ΠΏΠ΅Ρ€Π²Ρ‹Π΅ 3 строчки, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΠ½ΡΡ‚ΡŒ, ΠΊΠ°ΠΊ выглядят значСния

Helaian notepad-cheat untuk prapemprosesan Data yang pantas

df.info() #ДСмонстрируСм ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°Ρ…

Helaian notepad-cheat untuk prapemprosesan Data yang pantas

Mari lihat nilai lajur:

  1. Adakah bilangan baris dalam setiap lajur sepadan dengan jumlah baris?
  2. Apakah intipati data dalam setiap lajur?
  3. Lajur manakah yang ingin kita sasarkan untuk membuat ramalan untuknya?

Jawapan kepada soalan ini akan membolehkan anda menganalisis set data dan melukis pelan secara kasar untuk tindakan anda yang seterusnya.

Selain itu, untuk melihat lebih mendalam pada nilai dalam setiap lajur, kita boleh menggunakan fungsi pandas describe(). Walau bagaimanapun, kelemahan fungsi ini ialah ia tidak memberikan maklumat tentang lajur dengan nilai rentetan. Kami akan berurusan dengan mereka kemudian.

df.describe()

Helaian notepad-cheat untuk prapemprosesan Data yang pantas

Visualisasi sihir

Mari kita lihat di mana kita tidak mempunyai nilai sama sekali:

import seaborn as sns
sns.heatmap(df.isnull(),yticklabels=False,cbar=False,cmap='viridis')

Helaian notepad-cheat untuk prapemprosesan Data yang pantas

Ini adalah pandangan singkat dari atas, sekarang kita akan beralih kepada perkara yang lebih menarik

Mari cuba cari dan, jika boleh, alih keluar lajur yang hanya mempunyai satu nilai dalam semua baris (ia tidak akan menjejaskan keputusan dalam apa cara sekalipun):

df = df[[c for c
        in list(df)
        if len(df[c].unique()) > 1]] #ΠŸΠ΅Ρ€Π΅Π·Π°ΠΏΠΈΡΡ‹Π²Π°Π΅ΠΌ датасСт, оставляя Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚Π΅ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… большС ΠΎΠ΄Π½ΠΎΠ³ΠΎ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ значСния

Kini kami melindungi diri kami dan kejayaan projek kami daripada baris pendua (baris yang mengandungi maklumat yang sama dalam susunan yang sama seperti salah satu baris yang sedia ada):

df.drop_duplicates(inplace=True) #Π”Π΅Π»Π°Π΅ΠΌ это, Ссли считаСм Π½ΡƒΠΆΠ½Ρ‹ΠΌ.
                                 #Π’ Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°Ρ… ΡƒΠ΄Π°Π»ΡΡ‚ΡŒ Ρ‚Π°ΠΊΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Π΅ с самого Π½Π°Ρ‡Π°Π»Π° Π½Π΅ стоит.

Kami membahagikan set data kepada dua: satu dengan nilai kualitatif, dan satu lagi dengan nilai kuantitatif

Di sini kita perlu membuat penjelasan kecil: jika baris dengan data yang hilang dalam data kualitatif dan kuantitatif tidak begitu berkorelasi antara satu sama lain, maka kita perlu memutuskan apa yang kita korbankan - semua baris dengan data yang hilang, hanya sebahagian daripadanya, atau lajur tertentu. Jika garisan berkorelasi, maka kami mempunyai hak untuk membahagikan set data kepada dua. Jika tidak, anda perlu terlebih dahulu menangani baris yang tidak mengaitkan data yang hilang dalam kualitatif dan kuantitatif, dan kemudian membahagikan set data kepada dua.

df_numerical = df.select_dtypes(include = [np.number])
df_categorical = df.select_dtypes(exclude = [np.number])

Kami melakukan ini untuk memudahkan kami memproses kedua-dua jenis data yang berbeza ini - kemudian kami akan memahami betapa lebih mudahnya ini menjadikan hidup kami.

Kami bekerja dengan data kuantitatif

Perkara pertama yang perlu kita lakukan ialah menentukan sama ada terdapat "lajur pengintip" dalam data kuantitatif. Kami memanggil lajur ini kerana ia memaparkan diri mereka sebagai data kuantitatif, tetapi bertindak sebagai data kualitatif.

Bagaimanakah kita boleh mengenal pasti mereka? Sudah tentu, semuanya bergantung pada sifat data yang anda analisis, tetapi secara umum lajur tersebut mungkin mempunyai sedikit data unik (dalam kawasan 3-10 nilai unik).

print(df_numerical.nunique())

Setelah kami mengenal pasti lajur pengintip, kami akan mengalihkannya daripada data kuantitatif kepada data kualitatif:

spy_columns = df_numerical[['ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1', 'ΠΊΠΎΠ»ΠΎΠΊΠ°2', 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3']]#выдСляСм ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ-ΡˆΠΏΠΈΠΎΠ½Ρ‹ ΠΈ записываСм Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΡƒΡŽ dataframe
df_numerical.drop(labels=['ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1', 'ΠΊΠΎΠ»ΠΎΠΊΠ°2', 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3'], axis=1, inplace = True)#Π²Ρ‹Ρ€Π΅Π·Π°Π΅ΠΌ эти ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ ΠΈΠ· количСствСнных Π΄Π°Π½Π½Ρ‹Ρ…
df_categorical.insert(1, 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1', spy_columns['ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1']) #добавляСм ΠΏΠ΅Ρ€Π²ΡƒΡŽ ΠΊΠΎΠ»ΠΎΠ½ΠΊΡƒ-шпион Π² качСствСнныС Π΄Π°Π½Π½Ρ‹Π΅
df_categorical.insert(1, 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2', spy_columns['ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2']) #добавляСм Π²Ρ‚ΠΎΡ€ΡƒΡŽ ΠΊΠΎΠ»ΠΎΠ½ΠΊΡƒ-шпион Π² качСствСнныС Π΄Π°Π½Π½Ρ‹Π΅
df_categorical.insert(1, 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3', spy_columns['ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3']) #добавляСм Ρ‚Ρ€Π΅Ρ‚ΡŒΡŽ ΠΊΠΎΠ»ΠΎΠ½ΠΊΡƒ-шпион Π² качСствСнныС Π΄Π°Π½Π½Ρ‹Π΅

Akhir sekali, kami telah mengasingkan sepenuhnya data kuantitatif daripada data kualitatif dan kini kami boleh bekerja dengannya dengan betul. Perkara pertama ialah memahami di mana kita mempunyai nilai kosong (NaN, dan dalam beberapa kes 0 akan diterima sebagai nilai kosong).

for i in df_numerical.columns:
    print(i, df[i][df[i]==0].count())

Pada ketika ini, adalah penting untuk memahami di mana lajur sifar mungkin menunjukkan nilai yang hilang: adakah ini disebabkan oleh cara data dikumpulkan? Atau bolehkah ia berkaitan dengan nilai data? Soalan-soalan ini mesti dijawab mengikut kes demi kes.

Jadi, jika kami masih memutuskan bahawa kami mungkin kehilangan data yang terdapat sifar, kami harus menggantikan sifar dengan NaN untuk memudahkan anda mengendalikan data yang hilang ini kemudian:

df_numerical[["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° 1", "ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° 2"]] = df_numerical[["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° 1", "ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° 2"]].replace(0, nan)

Π’Π΅ΠΏΠ΅Ρ€ΡŒ посмотрим, Π³Π΄Π΅ Ρƒ нас ΠΏΡ€ΠΎΠΏΡƒΡ‰Π΅Π½Ρ‹ Π΄Π°Π½Π½Ρ‹Π΅:

sns.heatmap(df_numerical.isnull(),yticklabels=False,cbar=False,cmap='viridis') # МоТно Ρ‚Π°ΠΊΠΆΠ΅ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ df_numerical.info()

Helaian notepad-cheat untuk prapemprosesan Data yang pantas

Di sini nilai-nilai di dalam lajur yang tiada harus ditandakan dengan warna kuning. Dan kini keseronokan bermula - bagaimana untuk menangani nilai-nilai ini? Sekiranya saya memadamkan baris dengan nilai atau lajur ini? Atau isikan nilai kosong ini dengan beberapa nilai lain?

Berikut ialah rajah anggaran yang boleh membantu anda memutuskan perkara yang boleh, pada dasarnya, dilakukan dengan nilai kosong:

Helaian notepad-cheat untuk prapemprosesan Data yang pantas

0. Alih keluar lajur yang tidak perlu

df_numerical.drop(labels=["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2"], axis=1, inplace=True)

1. Adakah bilangan nilai kosong dalam lajur ini melebihi 50%?

print(df_numerical.isnull().sum() / df_numerical.shape[0] * 100)

df_numerical.drop(labels=["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2"], axis=1, inplace=True)#УдаляСм, Ссли какая-Ρ‚ΠΎ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° ΠΈΠΌΠ΅Π΅Ρ‚ большС 50 пустых Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ

2. Padam baris dengan nilai kosong

df_numerical.dropna(inplace=True)#УдаляСм строчки с пустыми значСниями, Ссли ΠΏΠΎΡ‚ΠΎΠΌ останСтся достаточно Π΄Π°Π½Π½Ρ‹Ρ… для обучСния

3.1. Memasukkan nilai rawak

import random #ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ random
df_numerical["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°"].fillna(lambda x: random.choice(df[df[column] != np.nan]["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°"]), inplace=True) #вставляСм Ρ€Π°Π½Π΄ΠΎΠΌΠ½Ρ‹Π΅ значСния Π² пустыС ΠΊΠ»Π΅Ρ‚ΠΊΠΈ Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹

3.2. Memasukkan nilai tetap

from sklearn.impute import SimpleImputer #ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ SimpleImputer, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠΌΠΎΠΆΠ΅Ρ‚ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ значСния
imputer = SimpleImputer(strategy='constant', fill_value="<Π’Π°ΡˆΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ здСсь>") #вставляСм ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ SimpleImputer
df_numerical[["новая_ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1",'новая_ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2','новая_ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3']] = imputer.fit_transform(df_numerical[['ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1', 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2', 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3']]) #ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅ΠΌ это для нашСй Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹
df_numerical.drop(labels = ["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3"], axis = 1, inplace = True) #Π£Π±ΠΈΡ€Π°Π΅ΠΌ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ со старыми значСниями

3.3. Masukkan nilai purata atau paling kerap

from sklearn.impute import SimpleImputer #ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ SimpleImputer, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠΌΠΎΠΆΠ΅Ρ‚ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ значСния
imputer = SimpleImputer(strategy='mean', missing_values = np.nan) #вмСсто mean ΠΌΠΎΠΆΠ½ΠΎ Ρ‚Π°ΠΊΠΆΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ most_frequent
df_numerical[["новая_ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1",'новая_ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2','новая_ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3']] = imputer.fit_transform(df_numerical[['ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1', 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2', 'ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3']]) #ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅ΠΌ это для нашСй Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹
df_numerical.drop(labels = ["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3"], axis = 1, inplace = True) #Π£Π±ΠΈΡ€Π°Π΅ΠΌ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ со старыми значСниями

3.4. Masukkan nilai yang dikira oleh model lain

Kadangkala nilai boleh dikira menggunakan model regresi menggunakan model daripada perpustakaan sklearn atau perpustakaan lain yang serupa. Pasukan kami akan menumpukan artikel berasingan tentang cara ini boleh dilakukan dalam masa terdekat.

Jadi, buat masa ini, naratif tentang data kuantitatif akan terganggu, kerana terdapat banyak nuansa lain tentang cara melakukan penyediaan dan prapemprosesan data dengan lebih baik untuk tugas yang berbeza, dan perkara asas untuk data kuantitatif telah diambil kira dalam artikel ini, dan kini adalah masa untuk kembali kepada data kualitatif. yang mana kita pisahkan beberapa langkah ke belakang daripada yang kuantitatif. Anda boleh menukar buku nota ini sesuka hati anda, menyesuaikannya dengan tugasan yang berbeza, supaya prapemprosesan data berjalan dengan pantas!

Data kualitatif

Pada asasnya, untuk data kualitatif, kaedah One-hot-encoding digunakan untuk memformatnya daripada rentetan (atau objek) kepada nombor. Sebelum beralih ke titik ini, mari kita gunakan gambar rajah dan kod di atas untuk menangani nilai kosong.

df_categorical.nunique()

sns.heatmap(df_categorical.isnull(),yticklabels=False,cbar=False,cmap='viridis')

Helaian notepad-cheat untuk prapemprosesan Data yang pantas

0. Alih keluar lajur yang tidak perlu

df_categorical.drop(labels=["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2"], axis=1, inplace=True)

1. Adakah bilangan nilai kosong dalam lajur ini melebihi 50%?

print(df_categorical.isnull().sum() / df_numerical.shape[0] * 100)

df_categorical.drop(labels=["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2"], axis=1, inplace=True) #УдаляСм, Ссли какая-Ρ‚ΠΎ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° 
                                                                          #ΠΈΠΌΠ΅Π΅Ρ‚ большС 50% пустых Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ

2. Padam baris dengan nilai kosong

df_categorical.dropna(inplace=True)#УдаляСм строчки с пустыми значСниями, 
                                   #Ссли ΠΏΠΎΡ‚ΠΎΠΌ останСтся достаточно Π΄Π°Π½Π½Ρ‹Ρ… для обучСния

3.1. Memasukkan nilai rawak

import random
df_categorical["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°"].fillna(lambda x: random.choice(df[df[column] != np.nan]["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°"]), inplace=True)

3.2. Memasukkan nilai tetap

from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy='constant', fill_value="<Π’Π°ΡˆΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ здСсь>")
df_categorical[["новая_колонка1",'новая_колонка2','новая_колонка3']] = imputer.fit_transform(df_categorical[['колонка1', 'колонка2', 'колонка3']])
df_categorical.drop(labels = ["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3"], axis = 1, inplace = True)

Jadi, kami akhirnya mendapat pegangan mengenai nulls dalam data kualitatif. Kini tiba masanya untuk melakukan pengekodan satu-panas pada nilai yang terdapat dalam pangkalan data anda. Kaedah ini sangat kerap digunakan untuk memastikan algoritma anda boleh belajar daripada data berkualiti tinggi.

def encode_and_bind(original_dataframe, feature_to_encode):
    dummies = pd.get_dummies(original_dataframe[[feature_to_encode]])
    res = pd.concat([original_dataframe, dummies], axis=1)
    res = res.drop([feature_to_encode], axis=1)
    return(res)

features_to_encode = ["ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°1","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°2","ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°3"]
for feature in features_to_encode:
    df_categorical = encode_and_bind(df_categorical, feature))

Jadi, kami akhirnya selesai memproses data kualitatif dan kuantitatif yang berasingan - masa untuk menggabungkannya kembali

new_df = pd.concat([df_numerical,df_categorical], axis=1)

Selepas kami menggabungkan set data menjadi satu, kami akhirnya boleh menggunakan transformasi data menggunakan MinMaxScaler daripada perpustakaan sklearn. Ini akan menjadikan nilai kami antara 0 dan 1, yang akan membantu semasa melatih model pada masa hadapan.

from sklearn.preprocessing import MinMaxScaler
min_max_scaler = MinMaxScaler()
new_df = min_max_scaler.fit_transform(new_df)

Data ini kini sedia untuk apa sahaja - rangkaian saraf, algoritma ML standard, dsb.!

Dalam artikel ini, kami tidak mengambil kira bekerja dengan data siri masa, kerana untuk data tersebut anda harus menggunakan teknik pemprosesan yang sedikit berbeza, bergantung pada tugas anda. Pada masa hadapan, pasukan kami akan menumpukan artikel berasingan untuk topik ini, dan kami berharap ia akan dapat membawa sesuatu yang menarik, baharu dan berguna ke dalam hidup anda, seperti yang satu ini.

Sumber: www.habr.com

Tambah komen