Skip to content

Instantly share code, notes, and snippets.

@dakeshi19
Created May 10, 2020 09:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dakeshi19/7572c4b8170dc037f3fd75b9b7742c07 to your computer and use it in GitHub Desktop.
Save dakeshi19/7572c4b8170dc037f3fd75b9b7742c07 to your computer and use it in GitHub Desktop.
前捌きpandasスニペットなど
import pandas as pd
import glob
import sys
import itertools
import re
import json
import collections
import itertools
import io
# CSVやDataFrameを雑にJOINする ----------
def rakkan_join(*args, join='inner', **kwargs):
"""
ワイドな行となる契約明細テーブルが様々な事情により、契約明細1、契約明細2、契約明細3...のような複数ファイルに分割されている。
これを元の契約明細テーブルの形に戻す(元のレコードのキーに該当するカラムで結合する)
関数(ファイル名1(またはDataFrame) キー1 ファイル名2(またはDataFrame) キー2 ファイル名3 .... )
xyz = pd.read_csv('iris.csv')
#同じファイルなので、同じ内容が横方向に繰り返されるチープな例となるがご了承のこと
rakkan_join(xyz, 'Name', 'iris.csv', 'Name')
rakkan_join('iris.csv','Name','iris.csv','Name')
"""
a = [i for i in args]
d = []
for f, c in zip(a[0::2], a[1::2]):
#DataFrameであればそれ、そうでなければファイル名だと見なしてファイル読み込みする
_ = f if isinstance(f,pd.DataFrame) else pd.read_csv(f, **kwargs)
_ = _.set_index(c)
d.append(_)
return pd.concat(d, axis=1, join=join)
def parent_join(parent_file, *args, **kwargs):
"""
   契約明細 + 商品コードマスタ
+ 担当者コードマスタ
+ ...
のような、ハブとなるテーブルを中心とした関係の複数テーブル由来の複数CSVを結合して、
契約明細としての1エントリのデータを生成する
xyz = pd.read_csv('iris.csv')
#同じファイルなので、同じ内容が横方向に繰り返されるチープな例となるがご了承のこと
parent_join(xyz, (xyz, 'Name', 'Name'), (xyz, 'Name', 'Name'))
"""
pa = parent_file if isinstance(parent_file,pd.DataFrame) else pd.read_csv(parent_file, **kwargs)
for i in args: # i[0] ファイル名 1. left_on 2. right_on 3. how(省略可能)
how = 'inner' if len(i) == 3 else i[3]
f = i[0]
_ = f if isinstance(f,pd.DataFrame) else pd.read_csv(f, **kwargs)
pa = pd.merge(pa,_,left_on=i[1],right_on=i[2],how=how)
return pa
def junction_join(file1, juncfile, file2, junc1, junc2, **kwargs): #leftjuncとrightjuncは結合キー名のtupleを想定
"""
学生、科目、履修科目(学生-科目関係テーブル)のような、N対Nの関係のテーブル(中間テーブル(juncfile))を介したデータを
結合したデータを取得する
"""
_func = lambda f : f if isinstance(f,pd.DataFrame) else pd.read_csv(f, **kwargs)
file1 = _func(file1)
juncfile = _func(juncfile)
file2 = _func(file2)
wrk = pd.merge(juncfile,file1, how='inner', on=junc1).copy()
wrk = pd.merge(wrk, file2, how='inner', on=junc2)
return wrk
def junction_table_as_list(groupkey, reldf, left_on, dispdf, right_on, listcolname='_'):
"""
取引情報 - N個の契約明細  のような関係のテーブルについて、契約明細部分をdictのlistとして1カラムに押し込めたものを
親となる取引情報にくっつけたデータを生成する。
(正規化を崩して、取引情報エンティティとして見たデータを生成し、その後の処理で取り回ししやすいようにする。)
"""
d = pd.merge(reldf, dispdf, left_on=left_on, right_on=right_on,how='inner')
d[valcolname] = d.groupby(groupkey).apply(lambda s: {k: v for k, v in s.items()}, axis=1)
return d.groupby(groupkey)[listcolname].apply(list).reset_index()
def fuzzy_join(df1, df2, left_on, right_on, *, cutoff=0.6, **kwargs):
"""
2つのデータセットの間のある項目の間におおよその対応関係がありこれらをキーに結合したい。
ただし、結合のための項目に表記揺れがあることから精度に課題があっても構わないので、
これらをあいまいな条件で結合してどのようなデータが得られるか確認したい。
表記揺れの実例イメージ → ファイルAは電話番号に「-」あり、ファイルBは無し。
あいまいな条件で結合 → difflib.get_close_matchesを用いて、結合できそうな値のキーを結合する。
あいまいさのパラメータ: cutoffで設定
"""
import difflib
dummycol = '____'
d1 = df1.copy()
d2 = df2.copy()
on1 = left_on + dummycol
on2 = right_on + dummycol + dummycol
d1[on1] = d1[left_on].astype(str).fillna('')
d2[on2] = d1[right_on].astype(str).fillna('')
d2_on2_list = list(set(list(d2[on2])))
wrapper = lambda x: x[0] if len(x) > 0 else None
d1[dummycol] = d1[on1].apply(lambda x: wrapper(difflib.get_close_matches(x, d2_on2_list, n=1, cutoff=cutoff)))
df = pd.merge(d1, d2, left_on=dummycol, right_on=on2, how='inner', **kwargs) # 注: 空文字列どおしがマッチ扱いになるのはひとまず仕様通り
df.drop(columns=[on1,on2,dummycol],inplace=True)
return df
# 一括読み込み ---------------
# メモ1:本関数群の用途をふまえて、CSVの読み込み時に強制的にdtype=str,fillnaを適用する方針とした。
# メモ2: 一部の関数では、元のCSVファイルの全ての値が欠損値の列はあらかじめ削除して、レポート表示を簡潔ににしている。
def bulkread_csv(files, **kwargs):
"""
ファイル名のリストを受け取って、ファイル名をキーとするDataFrameのdictを形成
"""
return {f:pd.read_csv(f,dtype=str,**kwargs).fillna('') for f in files}
def read_samecsv(files, **kwargs):
"""
同じフォーマットのファイル名のリストを与えて1つのDataFrameに取り込み
"""
df_ = []
for f in files:
df_.append(pd.read_csv(f,dtype=str,**kwargs).fillna(''))
return pd.concat(df_)
# ユーティリティ ---------------
def roma(s):
"""
ヘボン式のローマ字変換
"""
_roma = "a,あ,i,い,u,う,e,え,o,お,ka,か,ki,き,ku,く,ke,け,ko,こ,sa,さ,shi,し,su,す,se,せ,so,そ,ta,た,chi,ち,tsu,つ,te,て,to,と,na,な,ni,に,nu,ぬ,ne,ね,no,の,ha,は,hi,ひ,fu,ふ,he,へ,ho,ほ,ma,ま,mi,み,mu,む,me,め,mo,も,ya,や,yu,ゆ,yo,よ,ra,ら,ri,り,ru,る,re,れ,ro,ろ,wa,わ,あ,a,い,i,う,u,え,e,お,o,か,ka,き,ki,く,ku,け,ke,こ,ko,さ,sa,し,shi,す,su,せ,se,そ,so,た,ta,ち,chi,つ,tsu,て,te,と,to,な,na,に,ni,ぬ,nu,ね,ne,の,no,は,ha,ひ,hi,ふ,fu,へ,he,ほ,ho,ま,ma,み,mi,む,mu,め,me,も,mo,や,ya,ゆ,yu,よ,yo,ら,ra,り,ri,る,ru,れ,re,ろ,ro,わ,wa".split(',')
roma = {k: v for k, v in zip(_roma[0::2], _roma[1::2])}
# ひらがな → ローマ字
x = roma.get(s)
if x:
return x
# ローマ字 → ひらがな
y = [k for k, v in _roma.items() if v == s]
if len(y) > 0:
return y[0]
else: #変換をあきらめる
return None
# DataFrameデータのprintの変種 ---------------
def print_ltsv(df):
"""
LTSV出力
*DataFrameをそのまま戻して「pipe」に対応している
"""
df.apply(lambda s: print("\t".join([x+":"+str(y) for x,y in zip(df.columns,s) ])),axis=1)
return df
def print_colnum_tsv(df):
"""
カラム番号型LTSV風出力
*DataFrameをそのまま戻して「pipe」に対応している
"""
df.apply(lambda s: print("\t".join([ str(i)+":"+str(item) for i,item in enumerate(s) ])),axis=1)
return df
def print_colnum_and_colname(df):
"""
カラム番号と項目名の対を出力
*DataFrameをそのまま戻して「pipe」に対応している
"""
cols = df.columns.values
print("\t".join([str(i)+":"+str(item) for i,item in enumerate(cols) ]))
return df
def to_stdout(df, **kwargs):
"""
ものぐさな to_csv
*DataFrameをそのまま戻して「pipe」に対応している
"""
df.to_csv(__import__('sys').stdout,**kwargs)
return df
def eachfile_to_oneline(files,opt_func=lambda s:s):
"""
複数のファイルに対して、
主要な制御文字などを取り除くとともに、1ファイルを1行に納めて、Unixパイプに流す(Unix系コマンドで活用をイメージ)。
※sedやtrで可能な範囲だが、明示的に処理するところがポイント。
※opt_funcパラメータで、変換関数を追加指定できる。
"""
def edit(data, opt_func):
def default_func(str):
_str = str #制御文字などの除去はTODO
return _str
dst = ''
c = itertools.count(1)
for l in data.split('\n'):
dst += opt_func(default_func(l)) + '【改行' + str(next(c)) + '】' #あえてのダサ【改行】
return dst
d = {}
for i in files:
with open(i) as f:
d[i] = edit(f.read(),opt_func)
for f, x in d.items():
print(f,'\t',x)
def head2tail(frm, num, df=None, file=None, **kwargs):
"""
Un*x系のtailとheadだといざという時にオプションを忘れてしまうので何かとめんどくさい「ファイルのN行目からX行抜き出す」の対応
d = pd.read_csv('iris.csv',dtype=str)
head2tail(2, 4, df=d)
print('aaaaa')
head2tail(2, 4,file='iris.csv')
"""
if isinstance(df,pd.DataFrame):
print_ltsv(df.iloc[frm - 1 : frm + num -1 ])
return None
if file:
f = file
else:
_ = sys.stdin.read()
f = io.StringIO(_)
df = pd.read_csv(f, dtype=str, **kwargs).dropna(how='all', axis=1).fillna('')
print_ltsv(df.head(frm + num -1 ).tail(num))
# 変わり種のgrep ------------
def funnygrep(re_str, files, **kwargs):
"""
正規表現を受け取って、
指定のCSVファイル中のどのカラムにマッチする値があるかを表示
grip = funnygrep
grip('(?:2.5|1.8)', ['iris.csv'])
"""
d = {f: pd.read_csv(f, dtype=str, **kwargs).dropna(how='all',axis=1).fillna('') for f in files}
for f, df in d.items():
df[df.apply(lambda s: any(s.str.contains(re_str)),axis=1)].pipe(print_ltsv) #print_ltsv参照
def junkgrep(grepstr, files, ratio=1, **kwargs):
"""
difflibのあいまいマッチングを利用したあいまいgrep
junkgrep('IrisSetosa', ['iris.csv'], ratio=0.8)
"""
d = {f: pd.read_csv(f, dtype=str, **kwargs).dropna(how='all', axis=1).fillna('') for f in files}
from difflib import SequenceMatcher
def _match(s,grepstr,ratio):
smlist = [SequenceMatcher(isjunk=None,a=grepstr,b=_s) for _s in s] #ゲシュタルトパターンマッチング"
return any([True if sm.ratio() >= ratio else False for sm in smlist])
for f, df in d.items():
df[df.apply(_match,axis=1,grepstr=grepstr,ratio=ratio)].pipe(print_ltsv) #print_ltsv参照
# ユーティリティ
def open_html(s):
import tempfile
import subprocess
import time
fname = ''
with tempfile.NamedTemporaryFile(suffix='.html',mode='w',delete=False) as fp:
fname = fp.name
fp.write(b'HHHHH')
subprocess.call("open " + fname, shell=True)
time.sleep(100)
# 複数ファイルを一括で読み込んでいろいろレポートする ----------------
def desc_colnames(files):
"""
複数ファイルを実際に読み込んで
それらのカラム名を並べて表示
また、同じカラム名を持つファイル名をまとめて表示
files = ['iris.csv', 'train.csv']
print(desc_colnames(files))
"""
work_ = {} # ファイル名:カラム一覧
for f in files: # このような用途の場合は、通常無駄な列が多くその切り分けに難儀する傾向があるので、ここでは値を持たない列は削除して対象から外す
work_[f] = list(pd.read_csv(f, dtype=str).dropna(how='all',axis=1).columns.values)
work2_ = {} # カラム名:そのカラム名を持つファイル名の一覧
for k, cols in work_.items():
for c in cols:
if not work2_.get(c):
work2_[c] = []
work2_[c].append(k)
return work_,work2_
def desc_cardinality(files, **kwargs):
"""
DataFrameのdescription関数のオレオレ版
DataFrame.descriptionは統計量が中心だが、こちらは
文字列データの特徴を掴むために、各カラムの値のバリエーション数などを表示
"""
import numpy as np
cardinality_ = {}
for f in files:
cardinality_[f] = pd.read_csv(f,dtype=str,**kwargs).dropna(how='all',axis=1).fillna('').apply(lambda s: [len(set(s)),np.max(s),list(s.mode())[0] ]).to_dict()
return cardinality_
def wordcount(files, **kwargs):
"""
出現単語(日本語をイメージしているので、形態素解析とともに、語幹化などをある程度考慮した上で)のカウント
ここまでの方針からインプットにはcsvファイルを想定しているが、そうでない関数にしておいてもよかったかもしれない。
(非csvファイルを扱う場合は、my_tknzr関数を利用すれば良い。)
files = ['iris.csv', 'train.csv']
print(wordcount(files))
"""
from janome.tokenizer import Tokenizer
tokenizer = Tokenizer()
import string
def my_tknzr(text):
"""
janomeを使ったトーカナイザー
In [16]: my_tknzr('すもももももももものうち ')
Out[16]:
({
'すもも': ['すもも', 'スモモ', 'スモモ', 'すもも'],
'もも': ['もも', 'モモ', 'モモ', 'もも'],
'うち': ['うち', 'ウチ', 'ウチ', 'うち']
},
['すもも', 'もも', 'うち']
)
"""
def _isnot_stopword(s): # 明らかなstopwordは取り除く(ための判定)
stopwords = list(string.punctuation + string.whitespace + '『』{}「」()[]、。')
if s in stopwords or re.compile('^[' + string.punctuation + string.whitespace + ']+$').match(s): # 「\」の扱いは怪しいかもしれない
return False
return True
t = tokenizer.tokenize(text)
words = {i.surface: [i.base_form, i.phonetic, i.reading, i.surface] for i in t if set(('助詞', '助動詞', '数')).isdisjoint(set(i. part_of_speech.split(','))) and _isnot_stopword(i.surface) }
if words:
return words, list(words.keys())
return {'': []}, []
import collections
d = {}
for f in files:
_ = pd.read_csv(f, dtype=str, **kwargs).dropna(how='all', axis=1).fillna('').apply( \
lambda s: my_tknzr(' '.join(s.to_list()))[1], axis=1).to_list() # my_tknzrに強く依存
d[f] = collections.Counter(sum(_, []))
print(d[f])
return ''
def csv_notnull_rep(files, **kwargs):
"""
このファイルって、カラムAが主キーだよ...みたいなことを言われたのに、実際はカラムAにnullのものがあった(ぉいぉい)みたいなところを解き明かす
csvのnot nullである列名とnullとなる値がある列を含む行のレポート
files = ['iris.csv', 'train.csv']
csv_notnull_rep(files)
"""
d = {}
for f in files:
df = pd.read_csv(f, dtype=str, **kwargs).dropna(how='all', axis=1) #全行欠損値の列はそもそもターゲットにしない
colcond = ~df.isnull().any(axis=0)
print('# not nullである列名一覧')
print('#',f,list(colcond[colcond == True].index))
null_included = df.isnull().any(axis=1)
print('# null値を含む列の例' )
print('#',f,list(colcond[colcond == False].index))
print_ltsv(df[null_included])
def リレーション可能性調査(files, **kwargs):
"""
あるトランザクションに関する正規化された業務テーブルA〜EまでをそれぞれCSVファイルで抜き出したファイルを受領した。
シンプルなので、見れば分かると言われた言われたものの、怪しいので、少なくともこれらの間に結合できるような関係があるのかざっくり確認する。
【アプローチ】 指定された複数ファイルの全カラムのカーディナリティなどを調べて互いに結合できるか大雑把にチェックする
print(リレーション可能性調査(['iris.csv', 'iris.cp.csv']))
"""
setdict = lambda df: df.apply(lambda s: set(s)).to_dict() # DataFrameの各カラムのユニークな値のsetを取得する
d = {f: pd.read_csv(f, dtype=str, **kwargs).dropna(how='all', axis=1).fillna(''). \
pipe(setdict) for f in files} # 対象のファイルの列ごとのユニーク値のsetをdictに保持
comb = itertools.combinations(files, 2) # ファイル名のコンビネーション
report = []
for i in comb: # 各ファイルについて
x, y = i[0], i[1]
# 値のバリエーションの一致の割合を求める
for col_x, cat_x in d[x].items():
for col_y, cat_y in d[y].items():
all = cat_x | cat_y
intsec = cat_x & cat_y
repkey = (x,col_x,y,col_y)
report.append([repkey,int(len(intsec)/len(all) * 100)])
return sorted(report,key=lambda _: _[1],reverse=True) # 一致度が高いものから降順にならべる
# その他(あるテーブルとあるテーブルの釣り合いなどを確認)---------------
def read_csv_and_get_id_tuple(file, idcols=None, **kwargs):
"""
貰い物のデータについて、お店名カラムがユニークキーになっていると聞いているけど、ホンマかいみたいなところを
掘り下げて確認するための関数
CSVを読み込んで、特定カラム(レコードの主キーとなるカラムなどを想定)のユニークな値の一覧を取得
(複合カラムに対応)
"""
df = pd.read_csv(file, dtype=str, **kwargs).fillna('')
if not idcols:
return set(list(df.iloc[:, [0]].apply(tuple, axis=1)))
if any(type(i) is str for i in idcols):
return set(list(df[idcols].apply(tuple, axis=1)))
return set(list(df.iloc[:, idcols].apply(tuple, axis=1)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment