Siz de benim gibiyseniz, veri kümelerinizdeki eksik verilerle en az bir kez uğraşmışsınızdır. Veya iki kez. Ya da birçok kez…
Bazen bu sinir bozucu NA'ları idare etmek için gereken tek şey onları bırakmak, yani eksik verileri içeren satırları kaldırmaktır. Ancak bu, özellikle zaman serisi verileri ve hatta finansal veriler için her zaman ideal olmayabilir. Tabii ki, bu sorun iyi çalışılmış ve bırakmanın pek çok alternatifi mevcut.
Bunlardan birkaçını (aşağıda listelenmiştir) inceleyeceğim ve artılarını ve eksilerini tartışacağım:
Bırakma
LOCF (ileriye taşınan son gözlem)
Ortalama (veya benzer) isnat
İnterpolasyon
Spoiler uyarısı: Her şeye uyan tek bir yaklaşım yoktur! LOCF'nin genellikle finans için iyi bir seçim olduğunu ancak dezavantajlarının da olduğunu savunacağım. Bunu aklımda tutarak, bunları sergilemek için kullanacağım yöntemleri ve verileri açıklamama izin verin.
Not: Bilgili olmak istenirse, 2-4 arasındaki yöntemlerin tümü bazı isnat örnekleridir.
Bir insanın ilk etapta bırakmayı neden önemsediğine dair bazı örneklerle başlayalım. Örnek vermek gerekirse, hiçbir sapma olmadan rastgele bir yürüyüş izlediğini varsayarak bazı aşırı basitleştirilmiş günlük hisse senedi fiyatı verileri oluşturdum (yani ortalama uzun vadeli fiyat sabit kalmalı) - yine de en doğru değil ama iyi huylu bir varsayım.
np.random.seed(10) # needed for reproducibility price_moves = 3*pd.Series(np.random.randn(100)) # generating random "steps" with 0 mean price_vec = 100 + price_moves.cumsum() # generating brownian motion price_vec.plot()
Konu oldukça iyi niyetli görünüyor.
Şimdi günlük fiyat farklarının ampirik ortalamasını bulmak istediğimizi varsayalım.
price_vec.diff().mean() #sample mean >0.20030544816842052
Açıkçası sıfırdan farklı, üretim serisinin aksine - ancak bu sadece örnek gürültü. Şimdiye kadar, çok iyi.
Şimdi birkaç veri noktasını silerek bu veriyi biraz deforme edelim:
price_vec_na_simple = price_vec.copy() price_vec_na_simple.iloc[90:95] = np.array([np.NaN, np.NaN, np.NaN, np.NaN, np.NaN]) # price_vec_na_simple.diff().mean() >0.1433356258183252
Hemen birkaç şeyin farkına varırız -
diff
vektörü açıkça NA'lar içerse de ortalama bir şekilde NA değildir
Ortalama daha önce elde ettiğimizden farklı
Şimdi #1 oldukça kolay - pd.mean
varsayılan olarak NA'yı otomatik olarak kaldırır.
Peki ya #2? Neyi hesapladığımızı yeniden düşünelim.
En azından NA'lar olmadan, ortalama fiyat farkının basitçe (price_vec[99]-price_vec[0])/99
olması gerektiğini göstermek kolaydır - aslında, fiyat farklarını topladığımızda, tüm "ara" parçalar birbirini götürür, bu şekilde (price_vec[1] - price_vec[0]) + (price_vec[2] - price_vec[1]) + ..
!
Şimdi, eksik veriler eklendiğinde, önce farkları alıp sonra NA
bırakırsak, bu iptal işlemi bozulur - bazı kolay matematik işlemleri artık hesapladığınızı gösterir (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93
.
Bunu göstermek için, (NA - any number)
NA olarak değerlendirildiğinden ve daha sonra düşürüldüğünden, şu iki terimin artık çıkarıldığına dikkat edin - price_vec[95] - price_vec[94]
ve price_vec[90] - price_vec[89]
.
Bunu doğrulayalım:
(price_vec[99] - price_vec[0])/99 >0.20030544816842052 (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 >0.1433356258183252
Artık işleri nasıl düzeltebileceğimiz daha açık hale geliyor - önce NA'ları bırakmamız ve sonra diff
gerekiyor -
price_vec_na_simple.dropna().diff().mean() >0.21095999328376203
Ortalama neredeyse olması gereken yere döndü; küçük bir tutarsızlık oluyor çünkü artık ortalamada daha az terim var - 99 yerine 94.
Tamam, öyle görünüyor ki, eğer sadece ortalamayı önemsiyorsak, sadece dropna
kullanmamızın bir sakıncası yok (doğru yaptığımız sürece)? Sonuçta 0.2
ile 0.21
arasındaki fark, ilk etapta açıkça bizim gürültü toleransımız dahilindedir. Pek de değil - hadi nedenini görelim.
LOCF, İleriye Taşınan Son Gözlem anlamına gelir. Bunun arkasındaki fikir son derece basittir - düzenli olabilecek veya olmayabilecek belirli zaman aralıklarında veri kaydedersem, belirli bir aralığın gözlemi eksikse, değişkenimizde hiçbir şeyin değişmediğini varsayarız ve onu son olmayanla değiştiririz. eksik değer (örneğin - [3, 5, NA, 8] → [3, 5, 5, 8]). Birisi şu soruyu sorabilir: Neden ilk etapta gözlemi eksik olan bir aralığı önemseyelim, yani onu “bırakma” yönteminde olduğu gibi silmiyoruz? Cevap, yukarıda bahsetmediğim "düşmenin" doğasında olan kusurda yatıyor.
Birden fazla miktarı, özellikle de sıcaklık ve nemin saatlik kayıtları gibi genellikle çok hızlı değişmeyenleri aynı anda kaydettiğinizi varsayalım. 10:00, 11:00 ve 12:00 için her iki değere de sahip olduğunuzu, ancak saat 13:00'te yalnızca nemin olduğunu varsayalım. O "satırı" siler misiniz - yani 13:00 için okumanız yokmuş gibi mi davranırsınız? Sadece iki değişkeniniz varsa sorun değil - potansiyel olarak değerli bazı bilgileri (13:00 nemi) kaldırmış olsanız bile. Ancak aynı anda bu tür birçok olaya veya birçok değişkene sahipseniz, bırakma işlemi size neredeyse hiç veri bırakmayabilir!
Çok cazip bir alternatif, 12:00 ile 13:00 arasında sıcaklıkta hiçbir şeyin değişmediğini varsaymaktır. Sonuçta, saat 12:30'da biri yanımıza gelip “şu anki sıcaklık nedir” diye sorsaydı, haklı olarak 12:00 okumasıyla cevap verirdik (tabii ki hemen yeni bir ölçüm alamasak da). ). Neden aynı mantığı 13:00 değeri için de kullanmıyoruz?
Öncelikle yeni bulduğumuz yaklaşımımızı önceki veriler üzerinde test edelim:
price_vec_na_simple.ffill().diff().mean() # ffill performs LOCF by default >0.20030544816842052
Görünüşe göre tam olarak eski değerimize kavuştuk! Ek olarak, fiyat farkı verileri üzerinde daha fazla araştırma yapmak istiyorsanız, her gün için bir girişi olduğundan, bu girişlerden beşi artık 0 olmasına rağmen artık daha "düzenli" görünüyor (neden? price_vec_na_simple.ffill().diff().iloc[90:95]
komutunu çalıştırmayı deneyin) price_vec_na_simple.ffill().diff().iloc[90:95]
kendiniz görün).
Ayrıca finansta eksik veriler ile aykırı veriler sıklıkla bir araya geliyor. Şunu örneklendireyim:
#inflate two observations, delete three next ones price_moves_na[90] += 20 price_moves_na[91] += 30 price_moves_na[92] -= 50 # to "deflate" the price shock back price_vec_na = (100 + price_moves_na.cumsum()) price_vec_na[92:95] = [np.NaN, np.NaN, np.NaN] price_vec_na.tail(20).plot() price_vec_na.diff().dropna().mean() >0.7093365245831178
Keskin bir fiyat artışının ardından verilerin 3 gün boyunca mevcut olmadığını görebiliyoruz. Bu göründüğü kadar "yapay" bir örnek değil! En azından bu borsada yükselişin ardından ticaretin durduğunu hayal edin. Sonra işler biraz sakinleşti, böylece fiyat normal rejimine geri döndü. Belki kademeli bir şeyler oldu. Yükseliş ile yükseliş sonrası arasındaki noktaları aslında "birleştiren" perde arkasında yaşananlar sakinleşiyor.Fakat bunu bilmiyorsunuz ve bunun için herhangi bir veriye sahip değilsiniz!
Elimizde yeni bir veri yoksa en doğal varsayım nedir? Veri oluşturma modelimizin temelde fiyat değişikliklerine dayandığını hatırlayın. Yani yeni bir veri yoksa fiyat hiç değişmiyor olabilir mi? Bu tam olarak LOCF'nin (İleriye Taşınan Son Gözlem) varsaydığı şeydir.
Meraklı bir okuyucu için bir yan not - belki de LOCF'nin neden hisse senedi fiyat verileri için özellikle uygun olduğuna dair daha temel bir görüş, bunun genellikle bir martingale olarak modellenmesidir. Kabaca söylemek gerekirse, martingale yarın için en iyi tahminimizin bugün gördüklerimiz olduğu bir şeydir veya E[x_{t+1} | x_t] = x_t
Tamam, gerçek verilere dönelim! LOCF'nin sonuçlarını hem görsel hem de sayısal olarak inceleyelim:
price_vec_na.ffill().tail(20).plot() price_vec_na.ffill().diff().mean() >0.20030544816842052
LOCF'nin artılarını ve eksilerini hemen görüyoruz (kelimenin tam anlamıyla)! Birincisi, ortalama, olmasını "beklediğimiz" yere, yani değişmemiş ampirik değere geri döndü. Ancak fiyatın “tipik” çizginin dışına çıktığı ve 94 ile 95. günler arasında yapay fiyat düşüşlerinin yaşandığı oldukça çirkin bir dönemi başlatıyoruz.
LOCF'den elde ettiğimiz sonuçları (ortalama) atıfla karşılaştıralım. Özellikle zaman serisi olmayan veriler için NA işlemede çok yaygın bir seçimdir. Ancak saf bir şekilde yapılırsa, finansal veriler için kullanıldığında birçok dezavantaja sahiptir.
Yalnızca tüm örneklerin ortalamasını kullanırsanız, bariz bir ileriye dönük önyargı ortaya çıkarırsınız; yani geçmiş değerleri atamak için gelecekteki verileri kullanırsınız.
Bir çeşit geriye bakma veya yuvarlanma ortalamasını kullanmak kesinlikle daha iyidir; ancak bazen daha önce tanımladığımız martingale "taban çizgisi" görünümüyle gerilime neden olabilir.
Bu konuyu biraz daha detaylı inceleyelim. Geriye dönük değerlendirmeyi eski verilerimiz üzerinde kullanacağım -
price_vec_na_impute = price_vec_na.copy() price_vec_na_impute[price_vec_na_impute.isna()] = price_vec_na.iloc[:90].mean() price_vec_na_impute.diff().mean() >0.20030544816842052
LOCF ile aynı şekilde “doğru” fiyat değişimi ortalamasını elde ederiz. AMA 91. ve 92. günler arasında, bazı açılardan daha önce yaptığımızdan bile daha kötü olan yapay bir fiyat düşüşü uyguluyoruz. Sonuçta bu olay muhtemelen olaylar sakinleştiğinde veya sonrasında gerçekleşti, oysa bu seferki her şeyin hemen normale döneceğini varsayıyor. Bunun dışında, pratikte geriye dönük inceleme penceresini a) son trendleri yakalayacak ve aynı zamanda b) uzun vadeli eğilimleri (olağan önyargı-varyans değiş tokuşu) yakalayacak şekilde dengelemek biraz zor olabilir.
Şimdi daha karmaşık bir görevi gerçekleştirmek istediğimizi varsayalım: Fiyat serilerinden birinde veya her ikisinde eksik veri olduğunda, ampirik verilerden iki varlığın fiyat hareketleri arasındaki korelasyonu çıkarmak. Elbette, bırakmayı hâlâ kullanabiliriz, ancak:
Kullanabilsek bile ideal mi?
ya çok fazla değişkenimiz varsa - o zaman en az bir NA içeren tüm satırları bırakmak bizi hiçbir veri olmadan bırakabilir!
Korelasyonu hesaplamak isteyebileceğiniz birçok neden vardır; bu, hemen hemen her çok değişkenli modelde EDA'nın ilk adımıdır, her türlü portföy yapımında oldukça yaygın olarak kullanılır, vb. Dolayısıyla bu sayıyı olabildiğince doğru bir şekilde ölçmek oldukça gerekli!
Örneklemek için, birinci değişkenle "yerleşik" korelasyonu 0,4 olan ikinci bir değişken oluşturalım. Bunu yapmak için bir çeşit Gauss Karışım Modeli kullanacağız. Aklınızda bulunabilecek resim, önemli bir risk faktörünü paylaşan, birbiriyle ilişkili iki hisse senedidir, ancak ikinci hisse senedi aynı zamanda birincisinde olmayan büyük bir risk faktörüne de maruz kalmaktadır. Örneğin Google ve Facebook'u düşünün; ilk faktör teknoloji sektörü hakkındaki genel duyarlılık, ikincisi ise rakip sosyal ağlarla rekabet olabilir.
np.random.seed(2) # needed to ensure a fixed second series price_moves_2 = pd.Series(np.random.randn(100)) price_vec_2 = 50+(0.4*price_moves/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum() # all this math to ensure we get a 0.4 "theoretical" correlation with the first one
“Temel” ampirik korelasyonu, yani NA'lar ve sıçramalar olmadan kontrol edelim.
pd.concat([price_vec, price_vec_2], axis = 1).diff().corr().iloc[0,1] >0.4866403018044526
Bu, "teorik" korelasyona makul ölçüde yakındır - korelasyonun ampirik ölçümünün oldukça büyük gürültüye eğilimli olduğu iyi bilinmektedir.
Bir sonraki adım olarak, durumu NA'larla inceleyeceğiz ancak aykırı değerler olmadan inceleyeceğiz. Ayrıca diff
önce ve sonra dropna
ne olacağını da karşılaştıracağız.
pd.concat([price_vec_na_simple, price_vec_2], axis = 1).diff().corr().iloc[0,1] # implicit dropna after diff >0.5022675176281746 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.5287405341268966
Her iki sonuç da daha önce elde ettiğimiz “ampirik” değere oldukça yakın ve çok da uzak değil. LOCF'nin ve atamanın da iyi performans gösterdiğini doğrulayalım:
pd.concat([price_vec_na_simple, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.5049380499525835 price_vec_na_simple_impute = price_vec_na_simple.copy() price_vec_na_simple_impute[price_vec_na_simple_impute.isna()] = price_vec_na_simple_impute.iloc[:90].mean() pd.concat([price_vec_na_simple_impute, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.4866728183859715
Yukarıdaki 4 sonucu karşılaştırdığımızda tüm yöntemlerin oldukça iyi performans gösterdiğini görüyoruz. Belki de aykırı durum için de aynı şeyi beklemeliyiz?
Tutarlı kalabilmek için ikinci fiyat serisini ilk yaşanan fiyat şoklarının aynısına maruz bırakmamız gerektiğini, ancak aşağıdaki NA'ları içermediğini unutmayın. Yukarıdaki örneğe dönecek olursak, ilk risk faktöründe bir artışa neden olan ve sonunda ilk varlığın alım satımını durduran büyük bir olay hayal edin. İkinci varlık da elbette bunları yaşayacak, ancak belki daha az ölçüde ve dolayısıyla herhangi bir durma ve dolayısıyla NA'lar gerçekleşmeyecek.
price_vec_na_2 = 50+(0.4*price_moves_na/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum()
Tüm yöntemlerimizin performansını tekrar karşılaştıralım -
pd.concat([price_vec_na, price_vec_na_2], axis = 1).diff().corr().iloc[0,1] >0.6527112906179914 pd.concat([price_vec_na, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7122391279139506
Bu, hem teorik hem de ampirik değer açısından oldukça büyük bir fark! LOCF ve impute'a ne dersiniz?
pd.concat([price_vec_na, price_vec_na_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.33178239830519984 pd.concat([price_vec_na_impute, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7280990594963112
Şimdi nihayet LOCF'nin değerinin ne olduğunu görüyoruz! Açıkça diğer tüm yöntemlerden daha iyi performans gösteriyor!
Elbette bu %100 sağlam değil. Birincisi, LOCF yaparak eksik veriler sona erdiğinde büyük bir fiyat düşüşü sağlıyoruz. İkinci fiyat vektöründeki bazı aykırı değerlerle örtüşmesi durumunda sonuçlar oldukça değişebilir. (*Okuyucu için bir alıştırma - price_vec_na_2[95]
fiyat hareketi üzerindeki işareti çevirin ve sonuçları nasıl etkilediğini kontrol edin). Örneğin, fiyatın en yüksek noktası olan price_vec_na[91]
ile sonrasındaki "normal" değer price_vec_na[95]
arasında enterpolasyon yapmak yerine, sadece bu fiyat düşüşünü uygulamanın "temiz" olup olmadığı tam olarak açık değildir. Ancak özellikle "canlı" kullanımda enterpolasyon pek mümkün değildir! Sonuçta, eğer bugün 93. günse, 95. günün sonunda kaydedilen gelecekteki değeri kullanarak nasıl enterpolasyon yapabiliriz? Tarihsel bir çalışma için elbette bu bir seçenek olmaya devam ediyor, ancak gerçek tahmin için bunun nasıl yorumlanacağı ve kullanılacağı da belirsizliğini koruyor! Sonuç olarak, zaman boyutu boyunca enterpolasyon mümkündür ancak biraz daha şüphelidir.
LOCF'nin neden genellikle finansal zaman serilerindeki eksik verileri ele almak için en çekici ve basit seçenek olduğunu tanıtmak ve savunmak için küçük bir örnek olay incelemesi vermeye çalıştım.
Özetlemek gerekirse, artıları:
Bazı eksileri:
Bir malzeme ticareti mağazasında çalışan bir uzman olarak, bunu neredeyse tüm çalışmalarımda etkili bir temel olarak kullanıyorum. Bazı durumlar elbette daha incelikli ölçümler gerektirir, ancak bunlar çok azdır ve genellikle bahsedilen diğer 3 yöntemin hiçbiri tarafından %100 "çözülmez".