0%

最近 survey 到了一個很方便的特徵工程工具,叫 featuretools,其特色是專門針對關聯式資料庫的資料表做自動特徵生成,其強大之處在於可以一次關聯到好幾張表,並且可指定多種特徵公式來自動產生特徵,以下來介紹我的學習心得。

首先一樣透過 pip 來安裝如下:

1
pip install featuretools

featuretools 裡面有個非常重要的概念叫 **DFS (Deep Feature Synthesis)**,中文叫深度特徵生成,一個很潮的名詞。其實概念很簡單,就是藉由著資料表間的主鍵與外鍵的關聯去自動產生關聯的特徵。看看下圖的官方例子 Customers 這張表就是透過 Customer idPurchases 產生關聯,並且利用 primitive 去產生特徵,所謂的 primitive 就好像各種不同的特徵產生公式,featuretools 已內建很多種可供選擇。這裏選用 Max 這一個 primitive,對 Purchase Amount 這個欄位計算其最大值,如此就產生以下結果了。

再來是簡單的程式使用例子,首先載入需要用到的程式模組,featuretools 也內建了方便的 demo 用資料集可以用來快速展現 DFS 的功能。

1
2
3
4
5
6
7
import featuretools as ft

datas = ft.demo.load_mock_customer()

sessions_df = datas['sessions']
customers_df = datas['customers']
transactions_df = datas['transactions']

sessions、customers、transactions 分別代表彼此有關聯的資料表,這裏皆以 pandas dataframe 的形式儲存。在 featuretools 定義裡這些表就是所謂的 entity,可以作為 DFS 的執行目標單位。以下為這三張表的部分記錄。

1
customers_df.head()

1
sessions_df.head()

1
transactions_df.head()


可以發現這些表彼此間有主鍵外鍵的關係,也因此我們也必須明確定義給 featuretools 知道,如以下所示。

1
2
3
4
5
# 定義 relationship
relationships = [
("sessions", "session_id", "transactions", "session_id"),
("customers", "customer_id", "sessions", "customer_id")
]

也別忘了定義 entity,除了要指名 entity 的 dataframe 之外,也要指定欄位做為主鍵。

1
2
3
4
5
6
# 定義 entity
entities = {
"customers" : (customers_df, "customer_id"),
"sessions" : (sessions_df, "session_id", "session_start"),
"transactions" : (transactions_df, "transaction_id", "transaction_time")
}

當 entity 與 relationships 都指定好了話,就可以執行看看 DFS 了,程式碼如下,第 4 行表示要被作用的目標 entity,第 5 行 agg_primitives 這裡我採用了內建的 Max primitive,第 6 行 max_depth 表示 DFS 要關聯的深度,即表示從當前的表中依照外鍵往回追朔到其他表的深度,例如 customers 要接到 transactions 要接續關聯 2 次,就像 customers -> sessions -> transaction

1
2
3
4
5
6
7
feature_matrix_customers, features_defs = ft.dfs(
entities=entities,
relationships=relationships,
target_entity="customers",
agg_primitives=["Max"],
max_depth=2
)

最後的結果如下,可以看到新的特徵欄位成功的出現在原來 customers 表上了,多了 Max 以及還有其他的時間欄位,時間欄位像是 MONTH, DAY, YEAR 是預設從 join_date 出來的,也是一種 primitive。總之,featuretools 還有非常多種參數可以調校,用得好的話真的事可以加快分析流程啊。

reference:

  1. https://www.featuretools.com/

TF-IDF 是一種在文字分析領域中用來評估一個關鍵字在一組文檔集合中對一份文檔關聯程度的技術,很常用於資訊檢索的任務,找出與關鍵字最為匹配的文檔。其核心數學公式可分為兩部份:

  • TF (Term Frequency)
    表示詞頻,即待測量的關鍵詞在一個文檔中出現的次數除以該文檔中的所有的單詞數,公式如下所示。TF 的值越高表示該關鍵詞與該文檔越為匹配。其中,分母的部分除以文檔中所有字數是為了文檔的正規化,防止文字數過多的文檔主宰了主要的權重。

  • IDF (Inverse Document Frequency)
    即文檔的總數除以所有出現該關鍵詞的文檔數,公式如下。即關鍵詞越集中出現在某幾個文檔則關聯分數越高,若關鍵詞很普遍地出現在各個文檔中,則表示該關鍵詞的重要性越低。

最後以上兩者相乘就是 TF-IDF 的最終公式了,公式如下所示。

接下來是程式實作,以下就是我 python 程式碼關鍵部分。裡面第 4 行 IDF 分母加 1 是為了防止出現0而造成無窮大的情況。

1
2
3
4
5
def compute_tf_idf(self, word, document):
freq_dict = self._document_to_freq_dict(document)
tf = freq_dict.get(word, 0) / sum(freq_dict.values())
idf = math.log10(len(self.documents) / (1 + self._count_doc_freq_by_word(word)))
return tf * idf

輸入文檔的部分則以 python list 表示,每個元素表示一個文檔,先以簡單的英文文字示範,文檔如下。

1
2
3
4
5
6
7
documents = [
"I want to adopt the dog",
"a apple a day keeps doctor away",
"I have a pen I have an apple",
"who is your daddy",
"daddy daddy daddy daddy daddy"
]

這次要評估的關鍵詞為 daddy,很明顯的一定是第五個文檔的 TF-IDF 值最高,再來第四個文檔次之,以下就來執行主程式來驗證評估的成果,主程式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import math

class TFIDFService:
def __init__(self):
self.documents = None

def set_documents(self, documents):
self.documents = documents

def _document_to_freq_dict(self, document):
return dict([(word, document.count(word)) for word in document])

def _count_doc_freq_by_word(self, word):
doc_count = 0
for document in self.documents:
if word in document:
doc_count += 1
return doc_count

def compute_tf_idf(self, word, document):
freq_dict = self._document_to_freq_dict(document)
tf = freq_dict.get(word, 0) / sum(freq_dict.values())
idf = math.log10(len(self.documents) / (1 + self._count_doc_freq_by_word(word)))
return tf * idf

def tokenize(documents):
documents = [document.split(" ") for document in documents]
return documents

def main():
documents = [
"I want to adopt the dog",
"a apple a day keeps doctor away",
"I have a pen I have an apple",
"who is your daddy",
"daddy daddy daddy daddy daddy"
]
# 分詞前處理
documents = tokenize(documents)

tf_idf = TFIDFService()
tf_idf.set_documents(documents)
for index, doc in enumerate(documents, 1):
tf_idf_value = tf_idf.compute_tf_idf("daddy", doc)
print("document{}'s tf-idf: {}".format(index, tf_idf_value))

if __name__ == "__main__":
main()

執行的結果如下,果然就如預期一樣,第五個文檔 TF-IDF 值是最高的,第四個文檔次之,而第 1 到第 3 個文檔沒有出現關鍵字所以是 0,以上就是 TF-IDF 的實作成果了。

1
2
3
4
5
document1's tf-idf: 0.0
document2's tf-idf: 0.0
document3's tf-idf: 0.0
document4's tf-idf: 0.0554621874040891
document5's tf-idf: 0.2218487496163564

reference

  1. https://zh.wikipedia.org/wiki/Tf-idf
  2. https://github.com/Mark1002/nlp-experiment

generator 是 python 程式語言中一項強大的訪問集合元素的一種方式,基本上要定義一個 generator 只要在 function 內使用 yield 關鍵字就可以了。最簡單的方式就如以下定義:

1
2
def foo():
yield "hello world!"

一個 generator 就完成了,可以看看呼叫的結果會回傳一個表示generator的物件。而且要注意的是呼叫變成generator的 function 時並不會執行 function body 中的程式邏輯。

1
2
foo()
<generator object foo at 0x103ee07d8>

要真正的觸發generator內部的程式邏輯,必須要使用迭代器的操作方式,像是使用 next、迴圈等,才會觸發程式邏輯。因為generator 本身就是個迭代器。就像如下的例子,要依序走訪 generator 呼叫 python 的內建函數 next就可以了。

1
2
f_iter = foo()
next(f_iter)

呼叫的結果不意外地 “hello world!” 就被印出來了。

1
2
next(f_iter)
'hello world!'

要稍微注意的是迭代器只能往前不會後退的特性,若元素皆走訪完在呼叫一次 next 時會噴出 StopIteration 錯誤。

接下來介紹 generator 的實作關鍵 yield 的使用方式,yield 一開始乍看之下會以為與一般 function 的 return 很像,但事實上卻很不同。
首先,當 function 內部使用到 yield時,這個 function 就被視為 generator 了,只有在迭代階段才會執行程式邏輯。而在迭代階段執行程式碼到 yield 那行時,yield 會使當下的函式運行暫停下來並保存當前所有的的運行狀態,接著 yield 會回傳當前的結果給迭代器,到下一個迭代階段程式才會從方才yield 中斷的地方回復執行。舉一個例子如下:

1
2
3
4
5
6
7
8
def alternate_zero_and_one():
while True:
state = 1
yield state
state = 0
yield state

zero_one_gen = alternate_zero_and_one()

這是一個會不停交替產生出 1 與 0 的 generator 函式,來看看以下執行 next 迭代的結果。

1
2
3
4
5
6
next(zero_one_gen)
1
next(zero_one_gen)
0
next(zero_one_gen)
1

當第一次迭代執行到第一個 yield 時,由於之前 state = 1 所以 yield 回給迭代器的值就是 1,之後到了第二個迭代,程式並不會從 function body 的第一行從頭執行,而是會從剛才第一個 yield 的中斷處往下開始執行。所以接著執行 state = 0,之後碰到第二個 yield 以此類推,暫停並回傳當前結果,到下一迭代再從暫停處回復執行。這樣不停的 0 與 1 迭代功能就完成了。

然而使用 generator 有什麼好處呢?以我的角度來看 generator 可以在一些需產生大量集合元素的任務派上用場。看看以下第一種用 list 實作產生大量集合元素的方法。可以看到裡面有一個區域變數 result_list,會一直將產生的字串加入到該 list 中,加完之後回傳整個結果 list。

1
2
3
4
5
6
7
8
9
10
def foo_list(num):
count = 0
result_list = []
while count < num:
count += 1
result_list.append("yo!")
return result_list

for m in foo_list(9999999):
print(m)

很明顯的這段程式碼最大的問題就是當指定的 num很大時,result_list 每次都儲存的結果會吃掉大量的記憶體資源,後果極可能造成程式 crash 掉。所以generator 就派上用場了,改寫程式如下。

1
2
3
4
5
6
7
8
def foo_gen(num):
count = 0
while count < num:
count += 1
yield "yo!"

for m in foo_gen(9999999):
print(m)

由於 generator 在迭代階段才運行程式邏輯的特性,所以根本不需要額外的區域變數來儲存每次迭代運算的結果,大大的降低記憶體資源的使用量,同時也可以達到與 list 方法一樣的結果。

最後 generator 的強大之處不只如此,python 的 concurrency 非同步 IO 基礎就是基於
generator 而實現的,之後就待我研究完在分享吧!

reference:

  1. http://www.runoob.com/python3/python3-iterator-generator.html
  2. https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do

singleton 模式指的是一個類別所實體化的物件永遠是唯一的,或者說此類別只可以實體化物件一次,不多也不少。因此,從程式任何地方所存取此類別的物件都是同一個物件,很適合運用在資料庫連線等唯一物件的情況。而且 singleton 模式不需要宣告成全域變數就可以達到全局存取的效果,是個非常好用的設計模式。而 python 實現 singleton 模式的模式非常多元,以下來介紹實現 singleton 的三種方式。

1. 標準方式
以下是類似 Java 的標準 singleton 模式,有一個表示私有的類別變數 _instance 儲存 singleton 模式的物件,並用一個靜態方法 get_instance() 來負責物件的取得與物件實體化。只不過 python 沒有真正私有的機制,所以還是可以存取與改變內部類別的屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SingleTon:
# 加底線表示為私有變數
_instance = None

@staticmethod
def get_instance():
if SingleTon._instance is None:
SingleTon()
return SingleTon._instance
# 此處應為私有的建構子
def __init__(self):
if SingleTon._instance is not None:
raise Exception('only one instance can exist')
else:
self._id = id(self)
SingleTon._instance = self

def get_id(self):
return self._id

2. 利用 python 模組全域特性
這招算是最簡單的一個技巧可以快速實現 singleton 模式,其利用 python 模組本身就是全域特性,先在一個模組裡先實體化一個物件,之後程式其他任何地方要用到這個物件那就只要 import 這個模組裡的物件就好了。

1
2
3
4
5
6
7
8
9
10
# singleton_module.py
class _SingleTon:
def __init__(self):
self._id = id(self)

def get_id(self):
return self._id

SingleTonObject = _SingleTon()
# from singleton_module import SingleTonObject

3. 使用 metaclass 改變類別定義行為
這招算是很進階的寫法,利用 metaclass 來預先改變定義類別的行為來達到 singleton 模式的效果。在 python 的世界裡,類別也是一種物件,而所謂的 metaclass 就是定義類別這個物件的類別。聽起來非常抽象,這篇參考有對 metaclass 做了一個詳盡的介紹。以下就是 metaclass 的寫法。第 13 行 metaclass=SingletonMetaclass 指定了 Singleton 這個類別定義的方式。裡面的關鍵是在第 6 行 __call__ 裡面,這個 __call__ 可以用來定義類別的建立與初始化行為,其中注意此處的self.__instance 的 self 指的可是被定義的類別本身實體,而不是類別實體化的物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SingletonMetaclass(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
# 決定若使用類別來建構物件時,該如何進行物件的建立與初始
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance

class Singleton(metaclass=SingletonMetaclass):
def __init__(self):
self._id = id(self)

def get_id(self):
return self._id

接下來展示一下三種方式的測試結果。這裡簡單定義兩個 python module,裡面都呼叫了相應的 singleton,來看看在不同的 module 裡是否都是指向同一個物件。

1
2
3
4
5
6
7
8
9
# module1.py and moudle2.py
from singleton_module import SingleTonObject
from singleton_meta import Singleton
from singleton_standard import SingleTon

def execute():
print("standard method: {}".format(SingleTon.get_instance().get_id()))
print("module method: {}".format(SingleTonObject.get_id()))
print("metaClass method: {}".format(Singleton().get_id()))

執行以下主程式來查看結果。

1
2
3
4
5
6
7
8
import module1
import module2

if __name__ == '__main__':
print('module1:')
module1.execute()
print('module2')
module2.execute()

可以發現這三種方法都成功了實現 singleton 模式,module1 與 module2 的物件都是指向同一個物件。

1
2
3
4
5
6
7
8
module1:
standard method: 4301794776
module method: 4301894488
metaClass method: 4301795168
module2
standard method: 4301794776
module method: 4301894488
metaClass method: 4301795168

2019/05/26 補充:
經過多個月後發現其實 python3 的內部機制就可以辦到了….Orz,關鍵就是利用 __new__ 來去改變物件的建構行為。注意: python 真正建構物件的方法其實是 __new__, __init__ 則是負責對建構後的物件值初始化,所以在 __init__ 之前物件已經建立了。 而且__new__本身就是 static 方法,所以像之前方法一的方式可以完全被取代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SingleTonNew: 
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self, a, b):
self.a = a
self.b = b

s1 = SingleTonNew(a=11, b=21)
s2 = SingleTonNew(a=11, b=21)
id(s1)
id(s2)

一樣可以實現單例模式,且更簡潔。

1
2
4468430664
4468430664

reference:

  1. https://juejin.im/post/5a64255c51882573432d42e0
  2. https://gist.github.com/pazdera/1098129
  3. https://gist.github.com/werediver/4396488
  4. https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python
  5. https://www.code-learner.com/how-to-use-python-__new__-method-example/

pandas 是 python 資料分析中資料前處理的神器,實務上在進行資料分析專案中往往 80% 時間都是在清理資料,剩下 20% 的時間才是在套用分析演算法,可見清理資料是如此重要的事情,本篇記錄我 pandas 的一些學習心得。

資料 NA 值統計

利用以下程式技巧可以快速觀察資料中所有特徵的缺值記錄數。

1
2
missing_values_count = df.isnull().sum()
missing_values_count[:10]
1
2
3
4
5
6
7
8
9
10
11
Date                0
GameID 0
Drive 0
qtr 0
down 61154
time 224
TimeUnder 0
TimeSecs 224
PlayTimeDiff 444
SideofField 528
dtype: int64

去除 NA 值

此方法預設會以一列一列 (row) 的方式 axis=0 由上往下去除含有 NA 值的該筆記錄。

1
df.dropna()

指定參數 axis=1 的話則去除 NA 值的方向會變成以一行一行 (column) 的方式由左至右去除掉含有 NA 的欄位。

1
df.dropna(axis=1)

資料自動填補 NA 值的操作

此方法會自動將資料中的 NA 值替換成 0。

1
df.fillna(0)

也可以選擇其他補值的方法,例如採用以一列列的方式由後面的有值記錄補向前面缺值的記錄。

1
subset_df.fillna(method = 'bfill', axis=0).fillna(0)

時間序列解析度操作

時間序列解析也是 pandas 的強項,像是我常用的時間序列 resample 功能,可以很方便的變換記錄的間隔單位。

例如有如下的 df 資料

1
2
3
4
5
6
                   a
2018-03-18 -0.159224
2018-03-19 0.836197
2018-03-20 0.168442
2018-03-21 0.147137
2018-03-22 -0.605849

可以輸入以下程式碼來將時間間隔放大,從一天變兩天。

1
df.resample('2D').asfreq()

可以發現間隔變為兩天,同時資料量減少。

1
2
3
4
                   a
2018-03-18 -0.159224
2018-03-20 0.168442
2018-03-22 -0.605849

同樣的道理間隔變小,則產生的記錄會變多了,如以下改成每 12 小時一個區間如下

1
df.resample('12H').asfreq()

asfreq 只會單純的把時間序列拉長,不對資料數值做修改,因此新產生紀錄值會保持空的。

1
2
3
4
5
6
7
8
9
10
                            a
2018-03-18 00:00:00 -0.159224
2018-03-18 12:00:00 NaN
2018-03-19 00:00:00 0.836197
2018-03-19 12:00:00 NaN
2018-03-20 00:00:00 0.168442
2018-03-20 12:00:00 NaN
2018-03-21 00:00:00 0.147137
2018-03-21 12:00:00 NaN
2018-03-22 00:00:00 -0.605849

若不想產生 NA 值可以改用其他函式來產生重新 resample 後的時間序列資料。

1
df.resample('12H').bfill()

如此資料就可以由後往前來填補了。

1
2
3
4
5
6
7
8
9
10
                            a
2018-03-18 00:00:00 -0.159224
2018-03-18 12:00:00 0.836197
2018-03-19 00:00:00 0.836197
2018-03-19 12:00:00 0.168442
2018-03-20 00:00:00 0.168442
2018-03-20 12:00:00 0.147137
2018-03-21 00:00:00 0.147137
2018-03-21 12:00:00 -0.605849
2018-03-22 00:00:00 -0.605849

資料分割選擇與合併

資料的分割與選擇 pandas 主要可使用以下兩種方法:

iloc
iloc 是以 position-based index 的資料選擇方法,以數字來做為行與列的選擇。
loc
loc 則是以 label-based index 的方式選擇,將 index 以字串匹配的方式來選擇第幾行與第幾列。

如下參考圖所示:

首先令測試的 df 如下所示,維度為 4 x 4 的資料。

1
2
3
4
5
	        a	       b	       c	       d
0 -0.400363 1.141485 0.821102 0.237137
1 -0.122230 0.293221 -1.318223 0.765636
2 1.530837 -0.730529 -0.614539 -0.639149
3 -0.201329 1.279875 1.875210 0.466313

使用 iloc 選擇資料範圍到前 2 列,欄位第 1, 3 欄程式碼如下,可以發現 iloc 的 position 如同一般的陣列一樣 index 從 0 開始。

1
df.iloc[:2,[0,2]]

以下成功的選取到指定的資料。

1
2
3
               a	       c
0 1.574904 -0.263963
1 -1.377969 2.188388

改用 loc 時,要以 label 的思維來進行資料的選取,以下是選用與 iloc 同樣行列範圍的程式寫法。

1
df.loc[:1,['a', 'c']]

結果如下,亦可以成功的選取到與上例一樣的資料,但要注意的事當 df 的 index 是數值表示時 (如同本例的 df index: [0,1,2,3]),loc 一樣會把看似數值表示的 index 以 label 字串匹配的角度來進行資料選取。這也是為什麼 loc 的 row number range 是 1,而 iloc 卻是到 2,因為 loc 把 1 當成是字串 label 來選取了。

1
2
3
               a	       c
0 1.574904 -0.263963
1 -1.377969 2.188388

合併 concat
合併也是 pandas 常見的任務,利用合併更多的資料欄位來進行分析來取得更好的結果。以下來紀錄 pandas 中 concat 涵式的用法。
定義以下的兩個 df,為度分別為 4 X 5以及6 X 4來進行合併操作。

1
2
3
4
df1 = pd.DataFrame(np.random.randn(4,5), columns=['a', 'b', 'c', 'd', 'e'])
df2 = pd.DataFrame(np.random.randn(6,4), columns=['g', 'y', 'k', 'f'])
print(df1.shape)
print(df2.shape)
1
2
(4, 5)
(6, 4)

首先是列方向的合併,看到參數 axis=0 就可以立馬連想到是列的方向。

1
2
3
# 列方向合併 (上下)
merge_df = pd.concat([df1, df2], axis=0)
merge_df

程式執行如以下所示,可以發現兩個 df 確實的上下合併了,而且若沒有共同的欄位則會在新的 df 中全部補上並設為 NA 值。

1
print(merge_df.shape)
1
(10, 9)

最後行的合併方法如下,設 axis=0 來左右方向合併。

1
2
3
# 行方向合併 (左右)
merge_df = pd.concat([df1, df2], axis=1)
merge_df

可以發現兩個 df 也確實的左右合併了,而且合併後的 df 的長度會以原先被合併中最長的 df 為主,產生的空缺部分也一樣自動設為 NA 值。

1
merge_df.shape
1
(6, 9)

最後附上我 github 上面的 pandas 程式練習參考

reference
https://www.kaggle.com/rtatman/data-cleaning-challenge-handling-missing-values
http://www.learningthemachine.com/2017/08/20/review-pandas-loc-vs-iloc/

InfluxDB 是專門做用於 time series 的資料庫,由 go 語言編寫,擁有對 time series data 操作的強大特性,據統計也是目前最流行的 time series 資料庫,以下記錄我一些學習的心得。

nfluxDB 存儲的數據從邏輯上劃分可以分成以下四部分:

  1. measurement
    可以把 measurement 想成對應到 sql 的資料表,表示整個 time series data。
  2. time
    即表示時間戳記,視為 primary index。
  3. field
    表示一個或多個 <key,value> 的欄位,儲存該時間點所在的屬性與其數值,像是 <temparture, 28.5> 是 InfluxDB 真正儲存資料的地方,不可以省略該欄位,每一個記錄都對應一筆 timestamp。
  4. tag
    也表示一個或多個 <key,value> 的欄位,但型態皆是字串,與 field 不同。用來描述該筆時間點記錄的其他性質。會有這個 tag 欄位屬性主要是因為 time series data 除了每筆記錄都有 timestamp 外,往往還伴隨著其他屬性,像是 IOT 感測器的 device_id 金融資料的 acount_id 等。這個欄位也是 optional 的,用來輔助 query 資料用。

以上的四部分所組成的一筆記錄就視為 time series 中的一個資料點,以下為一筆記錄的表示形式:

1
measurement-name tag-set field-set timestamp

對應實際的例子如下:

1
cpu host=serverA,region=uswest idle=23,user=42,system=12 1464623548s

接下來介紹如何用程式來操作。InfluxDB 有提供 python 的 client library,以下就使用此 library 來進行 demo。首先,載入模組以及準備要新增的資料。可以看出這就是 measurement、tags、time、fields 標準格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from influxdb import InfluxDBClient

json_body = [
{
"measurement": "electric_power",
"tags": {
"device_id": "BB12IIMSG-1059010201",
"location": "Taipei"
},
"time": "2017-11-10T23:00:00Z",
"fields": {
"W": 50.64
}
},
{
"measurement": "electric_power",
"tags": {
"device_id": "RR72WWBBG-40190123456",
"location": "U.S.A"
},
"time": "2017-11-11T03:00:00Z",
"fields": {
"W": 60.88
}
}
]

接下來連接資料庫與建立資料庫,並寫入資料。沒意外的話這時已經順利新增兩筆 time series 記錄了。

1
2
3
client = InfluxDBClient('localhost', 8086, 'root', 'root', 'example')
client.create_database('example')
client.write_points(json_body)

接下來是查詢資料的部分,influxDB 也有支援類似 SQL 的語法查詢,這時 tag 就派上用場了,tag 可以做為 SQL 語法 where 的查詢條件。例如我們以 device_id 做為查詢條件。

1
2
3
result = client.query("select W from electric_power where device_id='RR72WWBBG-40190123456';")
for point in result.get_points():
logging.info(point)

結果如下,可以看到成功的查到對應的資料。

1
{'time': '2017-11-11T03:00:00Z', 'W': 60.88}

還有要注意的是 influxDB 查詢時的 SQL 欄位是一定要有 field 欄位在裡面的,如果只有 tag 欄位則無法查到任何記錄。例如以下的查詢少了 fields 欄位 W,則此查詢不成立。

1
client.query("select device_id from electric_power where location='Taipei';")

試用 InfluxDB 的感想就是使用上簡單又直覺,完全針對 time series data 去做設計,真的比用關聯式資料庫還要事先定義 schema 去存方便多了。

以下也附上我程式的執行範例
https://github.com/Mark1002/influxdb-survey/tree/master

reference:

  1. https://www.influxdata.com/time-series-database/
  2. time series DB 排名

時間序列資料 (time series) 與一般資料最大的不同處在於時間序列的前後每一筆記綠都有相依性,不可以將每一筆紀錄當成空間中獨立的點來看。要了解時間序列資料 (time series) 必須先從以下的特性開始。

time series 分解

首先,時間序列可以分解成以下三種特性:

  1. stationary
    中文譯為穩健性,表示時間序列資料的標準差以及平均值不會隨著時間推移而變化,而是保持著一個固定數值的狀態。stationary 是時間序列中非常著要的性質,有些預測的方法 (ARIMA) 根本上是基於此特性來去預測的。另一種 stationary 的說法為時序資料去除掉 Seasonality 與 trend 就會有 stationary 的性質了。
  2. seasonality
    表示時間序列中有相似的模式並週期性的循環出現。
  3. trend
    表示隨著時間的推移,時間序列有著明顯方向的增長或遞減。

而時間序列又可以分解成 additivemultiplicative 兩種不同的性質,兩者分解的公式如下:

  • additive:
    yt = St + Tt + Et

  • multiplicative:
    yt = St x Tt x Et

根據我對參考資料的理解,additive 特性的時間序列通常是代表有固定起伏、固定週期模式的線性時間序列。然而 multiplicative 則是相反,時間序列的起伏、週期會隨著時間不同成長幅度隨之變化,屬於非線性的時間序列,股票的時序資料多屬於此類。

利用以下 python 程式可以分解出 seasonality, trend, residual(stationary) 等時間序列不同部分。

1
2
3
4
5
6
from statsmodels.tsa.seasonal import seasonal_decompose

# Decompose time series
result = seasonal_decompose(etf_df['收盤價(元)'].values, 'multiplicative', freq=20)
result.plot()
plt.show()

time series data 相關性

時序資料也可以從過去幾筆間的資料來去看之間的相關性來做之後的為資料分析評估, 像是衡量Xt, Xt-1, Xt-2,...Xt-n間的關係,此性質叫 autocorrelation,表示時間序列資料與前筆資料的相關性程度。而最常用來檢視時間序列記錄關係性的功能就是 ACF (autocorrelation function) 了,用 ACF 可以來判斷這個時間序列資料是否有 stationarity 或 seasonality 的性質。

  • ACF plot:
    ACF 值介於 -1~1 間,越正相關越接近 1 ,反之越負相關則接近 -1,無相關則為 0,以下為 ACF 的示意圖,X 軸為 laged value,代表過去的時間記錄,Y 軸為 ACF 關係係數。這張圖表示該時間序列資料前 24 個 laged value 都高於 statistically significant 定值,有高度相關的關係,也因為有著高度相關的關係所以該時間序列為 non-stationary。

了解了以上的時間序列資料特性後就可以做時間序列預測 (time series forecasting) 的任務了,一些著名的統計預測方法,像是 ARIMA 就是基於以上的時間序列特性的,而時間序列要如何預測詳細待下回分解。

程式處理技巧

由於自己常常在處理 IOT sensor 的時間序列資料,也記錄一下常用的技巧。

  • time series data 區間間隔標準化
    sensor 資料的收集往往都會遇到網路延遲的問題,所以難保每筆記錄的時間間隔都是一樣的,pandas 提供了方便的技巧可以處理這類問題,如下程式碼所示。
    1
    2
    3
    4
    # regular time series interval to 1 min 且用最近的一筆記錄補值
    train_series_regular = train_series.resample('1T').bfill()
    # 間隔標準化後可能會產生缺值,因此在做內插
    train_series_regular = train_series_regular.interpolate(method='time', limit_direction='both')

參考:

  1. https://machinelearningmastery.com/feature-selection-time-series-forecasting-python/
  2. http://www.dummies.com/programming/big-data/data-science/autocorrelation-plots-graphical-technique-for-statistical-data/
  3. https://towardsdatascience.com/preprocessing-iot-data-linear-resampling-dde750910531
  4. https://datascopeanalytics.com/blog/unevenly-spaced-time-series/
  5. https://www.analyticsvidhya.com/blog/2016/02/time-series-forecasting-codes-python/
  6. https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-9-time-series-analysis-in-python-a270cb05e0b3
  7. https://www.otexts.org/fpp/6/1
  8. https://www.analyticsvidhya.com/blog/2018/02/time-series-forecasting-methods/

學習如果一知半解是件非常可怕的事。之前對於 python log 的使用就是這樣,導致程式寫完出了不少的包,因此我必須有必要重新理解 python logging 套件的使用機制。

遇到一個問題就是為什麼有加入了限制 log 檔大小的屬性 maxBytes 指定大小不超過 5 MB,但為什麼 log 檔案大小還是會無限制的增加呢?
以下是我原先的 django log 設定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '[ %(asctime)s; %(filename)s:%(lineno)d ] %(levelname)s:%(name)s: %(message)s'
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'log/debug.log',
'maxBytes': 5*1024*1024,
'formatter': 'standard',
},
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'standard',
},
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': True,
},
},
}

logging 負責寫入 log 檔的 handler 是由 RotatingFileHandler 這個類別來處理的。這個類別會將記錄存到 log 檔中,若裏頭存放的紀錄即將超過 maxBytes 的設定範圍時,則會備份舊的記錄到備份檔中,並且依照 backupCount 給定的數目來依序產生備份檔。像是有個 log 檔 app.log,若 backupCount=3 則將會依序產生 app.log.1, app.log.2, app.log.3 等備份檔,若這些備份都滿了,則會回到 app.log 從頭開始將新的記錄蓋過舊的記錄,所以故名思義真得跟 rotating ㄧ樣。

經過一方折騰終於發現問題出在哪了。以下是取自官方文件說明
Rollover occurs whenever the current log file is nearly maxBytes in length; but if either of maxBytes or backupCount is zero, rollover never occurs, so you generally want to set backupCount to at least 1, and have a non-zero maxBytes.
原因就是我沒有指定 backupCount, 而這個參數預設就是 0,沒有備份檔根本就不能將舊的記錄寫入,所以很自然的 log 檔就會越長越大了。
所以非常重要的一點是必須要加上 backupCount 這個參數,且值至少為 1,RotatingFileHandler 才可以根據 maxBytes 設定來將舊紀錄寫入備份檔。
再來值得注意的是,如果 log 是寫在 django 的話,則 django develop server 所對應的執行指令必須要加上 --noreload 參數,RotatingFileHandler 的設定才會有用,原因是 django 再 develop server 模式下會有兩個 thread,一個是執行 django 服務的主體,另一個則是監聽程式碼有沒有被修改,有修改則 reload 服務。所以若不把 reload 服務的 thread 關掉,則 log 檔就會被這個 thread 占住了,無法讓 RotatingFileHandler 對 log 檔進行大小限制處理,詳細指令如以下所示:

1
python manage.py runserver --noreload

所以我在 16 行加上了 backupCount 並指定為 1,如此就可以解決 django 中 log 檔無限制增加的問題了。

1
2
3
4
5
6
7
8
9
'file': 
{
'level': 'INFO',
'class':'logging.handlers.RotatingFileHandler',
'filename': 'log/debug.log',
'maxBytes': 5*1024*1024,
'backupCount': 1,
'formatter': 'standard',
}

reference:

  1. https://stackoverflow.com/questions/40088496/how-to-use-pythons-rotatingfilehandler
  2. https://stackoverflow.com/questions/46401571/python-3-logger-with-rotatingfilehandler-excedes-maxbytes-limit

python 程式語言的著名應用中莫過於就是寫網路爬蟲了,以下記錄我一些學習的心得。
首先,要利用 python 寫一隻爬蟲程式,裡面最重要的就是以下兩個 python 套件:

  1. requests
  2. BeautifulSoup

這兩個套件可以說是構成 python 網路爬蟲的基礎,requests 套件用來拜訪要爬資料的網站,並且取得爬資料網頁的整個 html 內容,BeautifulSoup 套件則是更進一步的剖析取回的 html 文檔內容,像是選擇特定幾個 tag 來取回想爬的資料。
以下為簡單的爬蟲範例,目標是爬一個登山資訊網站的資訊紀錄內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
from user_agent import generate_user_agent
import bs4

def hiking_crawler():
url = 'https://www.keepon.com.tw/forum-1-1.html'
headers = {
'User-Agent': generate_user_agent(device_type="desktop",
os=('mac', 'linux'))
}
response = requests.get(url, headers=headers)
page_content = bs4.BeautifulSoup(response.content, 'lxml')
hyperlink_list = page_content.select('#forum_list .media-body > h4 > a')

for hyperlink in hyperlink_list:
print(hyperlink.contents)

if __name__ == '__main__':
hiking_crawler()

第 8 行的 generate_user_agent 是用來模擬產生 http request header 中 User-Agent 的相關資訊,用來將爬蟲程式假裝成一般的瀏覽器使用者,如果沒有刻意加這個的話,User-Agent 預設就會透露爬蟲程式的資訊了,可能會被一些不想被爬的網站擋掉。第 12、13 行利用 BeautifulSoup 套件來將回應的 html 內容轉成專屬型別物件來進行之後的元素選擇操作,這裡我是用來取得登山資訊網站的超連結元素內容。最後的執行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
['劍龍稜新闢捷徑路線(避開重汙染區)+劍龍稜及鋸齒稜B段峭壁登架新繩']
['台南部分基石巡禮--大屯寮、山寮、林鳳營(未遇)、上茄苳、平頂山、六甲、頂秀祐與烏子嶺']
['北大武杷宇森出比魯溫泉']
['新北雙溪 蝙蝠山 苕谷瀑布 苕谷坑古道 苕谷坑山/東南峰 梅竹蹊山']
['新北雙溪 探訪古蹟古厝']
['水頭谷山東南峰']
['阿白縱走']
['坑內路森林步道/受天宮/北天宮行程 ']
['佛頂山朝聖寺/拐子湖山行程']
['連續摃龜的菜刀崙山北峰與車閂寮西北峰-大台北基石巡禮篇']
['20170624-27_干卓萬3.0之山豬驚魂']
['20171209-1214_南一三星好刺刺刺刺']

感想就是 BeautifulSoup 套件真是一個好東西。
參考:
https://www.kdnuggets.com/2018/02/web-scraping-tutorial-python.html

常說深度學習有三大網路,分別是 DNN、 CNN 與 RNN。DNN 就是一般類神經網路的多層網路,廣泛的應用在一般資料的機器學習任務。CNN 則是擁有捲積層的特化類神經網路,捲積層的特性使得這類網路在圖像辨識的應用上表現非常突出,一般有關圖像的任務大多使用 CNN 處理,至於 RNN 就是所謂的遞歸神經網路,擁有對過去歷史記錄記憶的能力,可以找出不同時間點記錄的行為模式,因此常應用於序列資料模式學習等相關任務,像是時間序列預測,語言翻譯等等。

而 LSTM 是其中一種 RNN ,由於其改進了傳統 RNN 的缺點,因此現在一般來說 LSTM 算是最常使用的 RNN 了,以下記錄我使用 kreas 實作 LSTM 的學習記錄:
資料我使用網路上參考的 sine-wave 資料,5001 筆記錄與 1 個特徵維度,視覺化資料為下圖所示:

要做的是對 sine-wave 預測資料走勢的任務,而要給 LSTM 的 input 資料必須要經過轉換才可做後續的訓練,參考 keras 的官方文檔可以得知 recurrent layers 接受的 input shape 為 (batch_size, timesteps, input_dim)batch_size 表示每次 LSTM 收到多少筆序列資料來執行梯度下降的序列資料筆數,換句話說 LSTM 基本的數入單位就是一筆序列記錄,timesteps 就是指該筆序列記錄從時間點 t 往回看多少單位時間點的長度,像是 timesteps=3 表示往回看了 t-2, t-1, t 共三筆記錄,而 input_dim 就是這個序列記錄的維度數,即有多少特徵,像是本資料的例子只有 1 個特徵,因此 input_dim =1

而要如何將原本的資料轉換成序列資料的形式呢?這裡的例子很簡單,概念就是去做 time slice window 轉換。如以下的程式碼所示設定window_size = 50將原本維度為 (5001, 1) 資料轉換乘 (4951, 51) 維度,這裡的 51 就是 timesteps 數,50 多了 1 是要做為預測目標變數 y 之用。

1
2
3
4
5
window_size = 50
series_s = series.copy()
for i in range(window_size):
series = pd.concat([series, series_s.shift(-(i+1))], axis = 1)
series.dropna(axis=0, inplace=True)

之後將轉換後的資料切分 train 與 test 資料:

1
2
3
4
5
6
7
8
9
10
11
12
nrow = round(0.8*series.shape[0])
train = series.iloc[:nrow, :]
test = series.iloc[nrow:,:]

train_X = (train.iloc[:,:-1]).values
train_y = (train.iloc[:,-1]).values
test_X = (test.iloc[:,:-1]).values
test_y = (test.iloc[:,-1]).values

# reshape to (batch_size, timesteps, input_dim)
train_X = train_X.reshape(train_X.shape[0],train_X.shape[1],1)
test_X = test_X.reshape(test_X.shape[0],test_X.shape[1],1)

接下來就是建立 model 的部分了,這裡建立了一個很簡單的 LSTM 模型,新版 keras 2.0 的 recurrent layers API 已可以不限制 input timesteps 的長度,所以 input_shape=(None,1),而 batch size 這裏則是在 fit 資料時設定。

1
2
3
4
5
6
# Define the LSTM model
model = Sequential()
model.add(LSTM(10, input_shape=(None,1)))
model.add(Dense(1))
model.compile(loss="mse", optimizer="adam")
model.summary()
1
2
3
4
5
6
7
8
9
10
-----------------------------------------------------------------
Layer (type) Output Shape Param #
=================================================================
lstm_47 (LSTM) (None, 10) 480
-----------------------------------------------------------------
dense_33 (Dense) (None, 1) 11
=================================================================
Total params: 491
Trainable params: 491
Non-trainable params: 0

經過一番參數調整後,最後模型的預測結果挺不錯的,連 mse 都小到趨近於 0 無法顯示而溢位了。

1
2
3
4
model.fit(train_X, train_y, batch_size=128, epochs=20, validation_split=0.1)
preds = model.predict(test_X)
actuals = test_y
mean_squared_error(actuals,preds)
1
6.0673873664723943e-06

視覺化出來預測結果與實際測試資料的預測變數 Y 分佈可以發現幾乎一模一樣重疊在一起了,證明了這個 LSTM 模型學習成功。

1
2
3
pyplot.plot(actuals)
pyplot.plot(preds)
pyplot.show()

reference:
https://machinelearningmastery.com/timedistributed-layer-for-long-short-term-memory-networks-in-python/