Higu`s diary

新米データサイエンティストのブログ。技術についてゆるく書きます〜

Wantedlyの機械学習エンジニアインターンに3週間いってきました

ひぐです!8/19~9/6の期間にWantedly社でMLエンジニアコースで働かせていただきました!
f:id:zerebom:20190908180454j:plain
楽しかったのでブログを書きたいと思います。

志望動機と選考

魔法のスプレッドシートでやれること・日程・給与などなどを見比べながら決めました。

2019夏のITエンジニアインターンの情報が集まる魔法のスプレッドシート - Google スプレッドシート

メンターさんが1on1で付いてくれること、サービスを知っているからこそ裏側を知るのが楽しそうと言った理由も大きかったです。

選考はES->コーディングテスト->Skype面接でした。
コーディングテストはAtCorder ABCのBくらい?の難易度でした。

何をしたか

Wanteldy Peopleのユーザにタグをつける。

Wantedly Peopleという名刺管理アプリケーションの改善するためDeepLearningでなんとかすることになりました。

people.wantedly.com

はじめに抽象度の高い課題をいくつか提示していただいて、 サーバーからデータをクエリで拾い、データを見ながら、実現可能性がありそうなアプローチを考えていくという段階からスタートしました。

僕が取り組んだタスクは、ユーザの職業欄からタグをつけるというものです。 例えば職業欄が
[取締役執行委員 社長]/[CEO]/[社長 取締役]⇨[社長]タグを付与
[〇〇営業所 部長]/[セールスエンジニア]⇨[営業]タグを付与

のようになります。要は名寄せとかカテゴリ付与みたいなタスクです。

名寄せができれば以下のメリットがあります

  • ユーザがフォロワーを検索するときの足がかりになる
  • 広告などのレコメンドのターゲッティングに使える指標となる

しかし、この課題には以下のような障害がありました

  • ユーザ一人当たりの情報が少ない
    読み込まれた名刺の持ち主はPeopleのユーザではないことが殆ど、プロフィール文やフォロワーの分布から予測はできない

  • 表記揺れがマジで多い
    社長を表す表現一つにとっても、「社長」「取締役代表」「代表取締役社長」「CEO」みたいに色々な種類がある
    「取締役代表 補佐」とか「取締役 秘書」とかは取り除かないといけないなどの問題も

最終的には
事前学習モデルから分散表現を獲得し、名刺に含まれる単語の平均ベクトルを学習して予測を立てる」 というアプローチになりました。

簡単にいうと、単語の意味を表すベクトルを使って、そのベクトルの近さとかでグループ分けをしようって感じです。

分散表現についてはこちらのサイトでわかりやすく説明されていました。
deepage.net

職業が同じ単語(「社長」や「代表取締役」など)は意味空間でのベクトルが近いので同じタグを貼れるだろうというアプローチです。
前処理はこんな感じです。
f:id:zerebom:20190907193758p:plain

今回はアノテーションはルールベースで行いました。 例えば名刺に「医」「療」という単語が含まれていれば医療ラベルを付与といった感じです。

結果としては予測精度は高かったのですが、精度の高さ=ルールをどれだけ守っているかになってしまい、 オフラインでの評価は難しかったです。

一応ルールベースでのアノテーションに変わる方法を何個か提示しました。 時間があれば半教師あり学習とかもできたらなぁって感じで終わりました。

機械学習エンジニアとして働くことを体験して

Kaggleなどでは決められたタスクに対して高速で実装していく力などが求められますが、
実務では、どんなアプローチがあって、どのデータなら使えるかなど、課題設定から始めなくてはいけないと改めて気づかされました。
また評価指標やアノテーションの仕方も考える必要があって、やりがいがありました。 そういった意味ではアイデア力も必要ですし、アイデアを産むために数理的な知識も、ビジネスの知識も必要だなぁと痛感しました。

こう言ったことを知れたのはメンターの縣さんが何を持ち帰ってもらおうか考えてインターン生をサポートしてくださったからこそだと思います。
本当にありがとうございました!

できるようになったこと・学んだこと

  • GItHubを綺麗に使えるようになった
    レポジトリを見てWantedly社は相当GItHubを使いこなしているなという印象を受けました。 他のレポジトリなどを見ながら、コードレビューしてもらいやすいプルリクの作り方や、後世に遺産が伝わりやすいIssueの書き方などを知れました。

  • 便利なオープンソースコードを知れた インターンでFastTextやBERTなどの事前学習モデルをクローンして使うことがあったのですが、あんなにお手軽に再学習や、分散表現を手に入れらるとは思いませんでした。 特にFastTextはgensimのWord2Vecクラスは便利なメソッドが多くて、今後も使おうと思います。
    またBigQueryとかPlotlyとかも書けるようになりました。

  • 機械学習エンジニアのリアルを知れた
    上記に書いたとおりです。

Wantedlyインターンの印象

たしかに就業型インターンでした

取り組んだ課題がインターン生のために用意された課題ではなく、インターン生を一人の社員として見てくださっている感じがありました。 給料をいただいているし、メンターの方の時間も割いていただきながら仕事をするので、進捗産まなきゃ。。。みたいな緊張感はありましたが、詰まったらいつでも聞いてねという感じだったのでありがたかったです。

オフィスも街も綺麗

オフィスが白金台だしピカピカだしテンション上がりました。   f:id:zerebom:20190908180530j:plain

あと会議室の名前が全部ジョジョのスタンドの名前でウケました。 「ザ・ワールドで話そうか」みたいな。出てこれなくなっちゃいそう。

業務後は卓球もできました。
f:id:zerebom:20190909084544j:plain

同じタームの子と仲良くなった

席が近い子とは毎日昼ごはんに行ったり、シャッフルランチが毎週あったりで仲良くなれました。
ただインターン生全員と席が近いわけではないので、全員と話すには割と能動的に行動しないといけないかもです。

f:id:zerebom:20190909083540j:plain
打ち上げのボドゲ大会

インターンの選考対策にしたこと

自分も去年はこういうブログを見て、インターン行きてえ。。。!ってなってたのですが、
「おすすめなので是非行ってみてください!」
的な文章を見るたびに、いや受かる前提やんけ!みたいな気持ちになってました。

誰得だよって感じですが、自分なりにES/面接で気をつけたことを書きたいと思います。

  • 成果ベースで話す
    〇〇を勉強中です!みたいなことを推すのは弱いと思ったので、なるべく成果ベースで話しました。 アウトプットが社外に公開されるような会社で長期インターンをしつつ、大きな仕事があれば積極的に関われるようにコミニュケーションを取るととっかかりやすいかもです。 あとはKaggleとかもメダルは取れたわけではないのですが、ちゃんとSubmitして順位が出ているので伝えました。

  • 自分の関心領域と業務内容が近い(近そう)ということを語る
    当たり前ですが、業務内容のミスマッチは企業側も避けたいと思っているので、
    自分の経験や興味が企業に取り組む内容に近いことを推していくのは大事だと思います。
    株主総会向けのプレゼン資料とかに、企業のビジョンや今後注力する領域がわかりやすくまとめてあるのでオススメです。

  • やる気を見せる
    記述式の問題や、ESの文字数制限ないところは人よりたくさん書いたと思います。

  • 最強の技術力があれば関係ない
    それはそう

  • 応募しないと絶対受からないので、いっぱい出す
    それはそう

終わり

こういったインターン行くと成長の糧になることをいっぱい学べるし、
仲間やメンターさんとのつながりもできるし、本当に参加できてよかったと思ってます!

今回学んだことを研究や将来の職業でも活かせるように今後も頑張ります!
3週間ありがとうございましたー!

GitHubに画像解析用のKerasディレクトリを公開しました。

お久しぶりです、ひぐです!

大学院に入ってからニューラルネットワークを使って、医療画像の補完を行う研究をするようになりました。

そこで今日は自分が普段使ってる研究用のコードを紹介したいと思います!
結構KaggleやQiitaとかでNNライブラリ用のソースコードを検索すると、使い切りなコードが多くないですか?

今回は繰り返し実験できるようにディレクトリごとコードを公開しました!
モデルの保存や、出力結果の記録をクラス単位で実装しています。よかったら参考にしてください。

なかなか上手に書けない部分もあるので、ご教授いただければ幸いです。。。笑
というかまだまだ絶賛修正中なので温かく見守ってくださいw

URLはこちら github.com

コードの概要

全体的にはこんなイメージです。 f:id:zerebom:20190518110310j:plain

主な機能は以下のようになっています。
* main.pyを走らせると自動で、loss関数のグラフ、出力画像を自動作成
* batch size training rateなどのハイパーパラメータははmain.py の引数で渡せる
* データをジェネレーターで読み込むのでデータ量が多くなってもメモリエラーにならない
* 結果を出力するディレクトリに使用したModelの名前とlossの値を使用する(ので見やすい)

main.py

import いろいろ(省略)

INPUT_SIZE = (256, 256)
CONCAT_LEFT_RIGHT=True
CHANGE_SLIDE2_FILL = True
def train(parser):
    configs = json.load(open('./settings.json'))
    reporter = Reporter(parser=parser)
    loader = Loader(configs['dataset_path2'], parser.batch_size)
    
    if CHANGE_SLIDE2_FILL:
        loader.change_slide2fill()
        reporter.add_log_documents('Done change_slide2fill.')

    if CONCAT_LEFT_RIGHT:
        loader.concat_left_right()
        reporter.add_log_documents('Done concat_left_right.')


    train_gen, valid_gen, test_gen = loader.return_gen()
    train_steps, valid_steps, test_steps = loader.return_step()

    # ---------------------------model----------------------------------

    input_channel_count = parser.input_channel
    output_channel_count = 3
    first_layer_filter_count = 32

    network = UNet(input_channel_count, output_channel_count, first_layer_filter_count)
    model = network.get_model()

    model.compile(optimizer='adam', loss='mse')
    model.summary()

    # ---------------------------training----------------------------------
    batch_size = parser.batch_size
    epochs = parser.epoch

    config = tf.ConfigProto()
    config.gpu_options.per_process_gpu_memory_fraction = 0.9
    config.gpu_options.allow_growth = True
    sess = tf.Session(config=config)

    # fit_generatorのコールバック関数の指定・TensorBoardとEarlyStoppingの指定

    logdir = os.path.join('./logs', dt.today().strftime("%Y%m%d_%H%M"))
    os.makedirs(logdir, exist_ok=True)
    tb_cb = TensorBoard(log_dir=logdir, histogram_freq=1, write_graph=True, write_images=True)

    es_cb = EarlyStopping(monitor='val_loss', patience=parser.early_stopping, verbose=1, mode='auto')

    print("start training.")
    # Pythonジェネレータ(またはSequenceのインスタンス)によりバッチ毎に生成されたデータでモデルを訓練します.
    history = model.fit_generator(
        generator=train_gen,
        steps_per_epoch=train_steps,
        epochs=epochs,
        validation_data=valid_gen,
        validation_steps=valid_steps,
        # use_multiprocessing=True,
        callbacks=[es_cb, tb_cb])

    print("finish training. And start making predict.")

    train_preds = model.predict_generator(train_gen, steps=train_steps, verbose=1)
    valid_preds = model.predict_generator(valid_gen, steps=valid_steps, verbose=1)
    test_preds = model.predict_generator(test_gen, steps=test_steps, verbose=1)

    print("finish making predict. And render preds.")

    # ==========================report====================================
    reporter.add_val_loss(history.history['val_loss'])
    reporter.add_model_name(network.__class__.__name__)
    reporter.generate_main_dir()
    reporter.plot_history(history)
    reporter.save_params(parser, history)

    input_img_list = []
    # reporter.plot_predict(train_list, Left_RGB, Right_RGB, train_preds, INPUT_SIZE, save_folder='train')
    reporter.plot_predict(loader.train_list, loader.Left_slide, loader.Left_RGB,
                          train_preds, INPUT_SIZE, save_folder='train')
    reporter.plot_predict(loader.valid_list, loader.Left_slide, loader.Left_RGB,
                          valid_preds, INPUT_SIZE, save_folder='valid')
    reporter.plot_predict(loader.test_list, loader.Left_slide, loader.Left_RGB,
                          test_preds, INPUT_SIZE, save_folder='test')
    model.save("model.h5")


def get_parser():
    parser = argparse.ArgumentParser(
        prog='generate parallax image using U-Net',
        usage='python main.py',
        description='This module generate parallax image using U-Net.',
        add_help=True
    )

    parser.add_argument('-e', '--epoch', type=int,
                        default=200, help='Number of epochs')
    parser.add_argument('-b', '--batch_size', type=int,
                        default=32, help='Batch size')
    parser.add_argument('-t', '--trainrate', type=float,
                        default=0.85, help='Training rate')
    parser.add_argument('-es', '--early_stopping', type=int,
                        default=20, help='early_stopping patience')

    parser.add_argument('-i', '--input_channel', type=int,
                        default=7, help='input_channel')

    parser.add_argument('-a', '--augmentation',
                        action='store_true', help='Number of epochs')

    return parser


if __name__ == '__main__':
    parser = get_parser().parse_args()
    train(parser)

ディレクトリのパスはsetting.jsonで一括管理しています。
trainという巨大な関数にargparserで引数を渡して、ハイパーパラメータを用いています。
自分の研究では、入力、教師データどちらにも画像を使うため独自のジェネレータを作成しています。

repoter.py

importあれこれ

class Reporter:
    ROOT_DIR = "Result"
    IMAGE_DIR = "image"
    LEARNING_DIR = "learning"
    INFO_DIR = "info"
    MODEL_DIR = "model"
    PARAMETER = "parameter.txt"
    IMAGE_PREFIX = "epoch_"
    IMAGE_EXTENSION = ".png"
    
    def __init__(self, result_dir=None, parser=None):
        self._root_dir = self.ROOT_DIR
        self.create_dirs()
        self.parameters = list()
    # def make_main_dir(self):

    def add_model_name(self, model_name):
        if not type(model_name) is str:
            raise ValueError('model_name is not str.')

        self.model_name = model_name
    def add_val_loss(self, val_loss):
        self.val_loss = str(round(min(val_loss)))

    def generate_main_dir(self):
        main_dir = self.val_loss + '_' + dt.today().strftime("%Y%m%d_%H%M") + '_' + self.model_name
        self.main_dir = os.path.join(self._root_dir, main_dir)
        os.makedirs(self.main_dir, exist_ok=True)

    def create_dirs(self):
        os.makedirs(self._root_dir, exist_ok=True)

    def plot_history(self,history,title='loss'):
        # 後でfontsize変える
        plt.rcParams['axes.linewidth'] = 1.0  # axis line width
        plt.rcParams["font.size"] = 24  # 全体のフォントサイズが変更されます。
        plt.rcParams['axes.grid'] = True  # make grid
        plt.plot(history.history['loss'], linewidth=1.5, marker='o')
        plt.plot(history.history['val_loss'], linewidth=1., marker='o')
        plt.tick_params(labelsize=20)

        plt.title('model loss')
        plt.xlabel('epoch')
        plt.ylabel('loss')
        plt.legend(['loss', 'val_loss'], loc='upper right', fontsize=18)
        plt.tight_layout()

        plt.savefig(os.path.join(self.main_dir, title + self.IMAGE_EXTENSION))
        if len(history.history['val_loss'])>=10:
            plt.xlim(10, len(history.history['val_loss']))
            plt.ylim(0, int(history.history['val_loss'][9]*1.1))

        plt.savefig(os.path.join(self.main_dir, title +'_remove_outlies_'+ self.IMAGE_EXTENSION))

    def add_log_documents(self, add_message):
        self.parameters.append(add_message)


    
    def save_params(self,parser,history):
        
        #early_stoppingを考慮
        self.parameters.append("Number of epochs:" + str(len(history.history['val_loss'])))
        self.parameters.append("Batch size:" + str(parser.batch_size))
        self.parameters.append("Training rate:" + str(parser.trainrate))
        self.parameters.append("Augmentation:" + str(parser.augmentation))
        self.parameters.append("input_channel:" + str(parser.input_channel))
        self.parameters.append("min_val_loss:" + str(min(history.history['val_loss'])))
        self.parameters.append("min_loss:" + str(min(history.history['loss'])))

        # self.parameters.append("L2 regularization:" + str(parser.l2reg))
        output = "\n".join(self.parameters)
        filename=os.path.join(self.main_dir,self.PARAMETER)

        with open(filename, mode='w') as f:
            f.write(output)

    @staticmethod
    def get_concat_h(im1, im2):
        dst = Image.new('RGB', (im1.width + im2.width, im1.height))
        dst.paste(im1, (0, 0))
        dst.paste(im2, (im1.width, 0))
        return dst

    def plot_predict(self, img_num_list, Left_RGB, Right_RGB, preds, INPUT_SIZE, max_output=20,save_folder='train'):
        if len(img_num_list) > max_output:
            img_num_list=img_num_list[:max_output]
        for i, num in enumerate(img_num_list):
            if i == 1:
                print(preds[i].astype(np.uint8))
                        
            pred_img = array_to_img(preds[i].astype(np.uint8))
           
            train_img = load_img(Left_RGB[num], target_size=INPUT_SIZE)
            teach_img = load_img(Right_RGB[num], target_size=INPUT_SIZE)
            concat_img = self.get_concat_h(train_img, pred_img)
            concat_img = self.get_concat_h(concat_img, teach_img)
            os.makedirs(os.path.join(self.main_dir,save_folder), exist_ok=True)
            array_to_img(concat_img).save(os.path.join(self.main_dir, save_folder, f'pred_{num}.png'))

データの保存を担っています。
Kerasではfit関数を動かすとその返り値にhistoryオブジェクトという出力のログが入ったインスタンスを返します。
これと、parserをmain.pyから受け取ってデータを保存しています。
保存先はResult dir以下に、使用パラメータ・出力結果・lossグラフなどをまとめて格納します。

f:id:zerebom:20190518112328p:plain

loader.py

import あれこれ

config = json.load(open('./settings.json'))


class Loader(object):
    # コンストラクタ
    def __init__(self, json_paths, batch_size, init_size=(256, 256)):
        self.size = init_size
        self.DATASET_PATH = json_paths
        self.add_member()
        self.batch_size = batch_size



    # def define_IO(self):
    def add_member(self):
        """
        jsonファイルに記載されている、pathをクラスメンバとして登録する。
        self.Left_RGBとかが追加されている。
        """
        for key, path in self.DATASET_PATH.items():
            setattr(self, key, glob.glob(os.path.join(path, '*png')))
    
    #左右の画像を結合してデータを二倍にする
    def concat_left_right(self):
        self.Left_slide += self.Right_slide
        self.Left_RGB += self.Right_RGB
        self.Left_disparity += self.Left_disparity
        self.Right_disparity += self.Right_disparity
        self.Left_bin += self.Left_bin
        self.Right_bin += self.Right_bin
        print('Done concat_left_right.')
    
    #入力で使う画像を平均値で埋めた画像にする
    def change_slide2fill(self):
        self.Left_slide = self.Left_fill
        self.Right_slide = self.Right_fill


    def return_gen(self):
        self.imgs_length = len(self.Left_RGB)
        # self.train_paths = (self.Left_slide, self.Right_disparity, self.Left_disparity)
        # sel = self.Left_RGB
        self.train_list, self.valid_list, self.test_list = self.train_valid_test_splits(self.imgs_length)
        self.train_steps = math.ceil(len(self.train_list) / self.batch_size)
        self.valid_steps = math.ceil(len(self.valid_list) / self.batch_size)
        self.test_steps = math.ceil(len(self.test_list) / self.batch_size)
        self.train_gen = self.generator_with_preprocessing(self.train_list, self.batch_size)
        self.valid_gen = self.generator_with_preprocessing(self.valid_list, self.batch_size)
        self.test_gen = self.generator_with_preprocessing(self.test_list, self.batch_size)
        return self.train_gen, self.valid_gen, self.test_gen

    def return_step(self):
        return self.train_steps, self.valid_steps, self.test_steps

    @staticmethod
    def train_valid_test_splits(imgs_length: 'int', train_rate=0.8, valid_rate=0.1, test_rate=0.1):
        data_array = list(range(imgs_length))
        tr = math.floor(imgs_length * train_rate)
        vl = math.floor(imgs_length * (train_rate + valid_rate))

        random.shuffle(data_array)
        train_list = data_array[:tr]
        valid_list = data_array[tr:vl]
        test_list = data_array[vl:]

        return train_list, valid_list, test_list

    def load_batch_img_array(self, batch_list, prepro_callback=False,use_bin=True):
        teach_img_list = []
        input_img_list = []
        for i in batch_list:
            LS_img = img_to_array(
                load_img(self.Left_slide[i], color_mode='rgb', target_size=self.size)).astype(np.uint8)
            LD_img = img_to_array(
                load_img(self.Left_disparity[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8)
            RD_img = img_to_array(
                load_img(self.Right_disparity[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8)

            if use_bin:
                LB_img = img_to_array(
                    load_img(self.Left_bin[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8)
                # LB_img=np.where(LB_img==255,1,LB_img)

                RB_img = img_to_array(
                    load_img(self.Right_bin[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8)
                # RB_img = np.where(RB_img == 255, 1, RB_img)

                input_img = np.concatenate((LS_img, LD_img, RD_img, LB_img, RB_img), 2).astype(np.uint8)
            else:
                input_img=np.concatenate((LS_img, LD_img, RD_img), 2).astype(np.uint8)


            teach_img = img_to_array(
                load_img(self.Left_RGB[i], color_mode='rgb', target_size=self.size)).astype(np.uint8)
               
            input_img_list.append(input_img)
            teach_img_list.append(teach_img)

        return np.stack(input_img_list), np.stack(teach_img_list)

    def generator_with_preprocessing(self, img_id_list, batch_size):#, *input_paths
        while True:
            for i in range(0, len(img_id_list), batch_size):
                batch_list = img_id_list[i:i + batch_size]
                batch_input, batch_teach = self.load_batch_img_array(batch_list)

                yield(batch_input, batch_teach)

class DataSet:
    pass

Data dirからデータ(画像)を読み取ってmain.pyにジェネレータ形式で渡します。
このコードは特にまだまだ改善の余地があります...

実験ごとに入力チャンネル数を変えたいのですが、
ジェネレータに読みだした後、それらを結合するとことが難しく、悩んでいます。

jsonからディレクトリのパスを受け取って、その直下の画像ファイルをすべてクラスメンバにして
読み込むようにしているのがおしゃれポイントです

        for key, path in self.DATASET_PATH.items():
            setattr(self, key, glob.glob(os.path.join(path, '*png')))

おわりに

ザックリですが以上になります!
わからないところや修正したほうがいいと思う部分がありましたら、連絡いただけたら幸いです!

今後は他の人でも使えるように、どんなタスクでも使えるように、調整して再度公開したいです。

就職したときにも、恥ずかしくないようにきれいで再利用性の高いコードをかけるように頑張っていきたいです!
では~

Santanderコンペで学ぶ、EDA(探索的データ解析)の手法

お久しぶりです、ひぐです!

f:id:zerebom:20190412103328p:plain  

先日、Kaggleの Santander Customer Transaction Predictionコンペティション(以下、Santanderコンペ)
に参加したのですが、凄惨な結果に終わってしまいました。。。

  そこで今回の記事では自分の復習もかねて、上位の方の解法をまとめつつ
データ分析に必要なEDAの手法を紹介していきたいと思います!  

この記事を読むのに必要な前提知識

  • Kaggleとは?
    インターネット上で開催されているデータ分析コンペティション
    企業が公開したデータセットを前処理、モデル構築を行い、
    予測データを作成し、その精度で競います。

くわしくは↓
www.codexa.net

  • EDAとは?
    Explanatory Data Analysis の略。日本語で言うと、探索的データ解析。
    データの特徴や構造を理解するためにグラフを作成し、
    特徴量の相関やターゲットとの関係性を調べることです。

くわしくは↓

www.codexa.net

Santanderコンペってどんなコンペ?

f:id:zerebom:20190412105118p:plain

Santanderというアメリカの銀行が開いたコンペ。

レーニング・テストデータどちらも
20万行、かつ200列の特徴量から構成されている。
特定の取引を行った行にtarget==1が付与され、それ以外にtarget==0が付与されている。

全ての特徴量がfloat型かつvar_iと命名されていて、どんな特徴量か分からない様に匿名化されている。
f:id:zerebom:20190412105125p:plain

どんなことに気づけばスコアが伸びたのか

このコンペでは大きく分けて、

  • testデータにある、計算に用いられない偽データの存在
  • 逆向きになっている列の存在(0行目の値が200000行目の値、1行目→199999,2→199998...)
  • 値が重複しているかどうかの特徴量
  • 各列が独立であるということ

これらに気づけた場合、精度があげることが出来たそうです(どうやってわかるんだよ

これらを踏まえてEDAしていきましょう。

1位の方の解法はこちら↓
#1 Solution | Kaggle

EDAの手法紹介

基礎編(どんなコンペでも用いるEDA)

基礎編ではこの方のKernelを参考にしました。ありがとうございます。
https://www.kaggle.com/gpreda/santander-eda-and-prediction

0.ライブラリimport

import gc
import os
import logging
import datetime
import warnings
import numpy as np
import pandas as pd
import seaborn as sns
import lightgbm as lgb
from tqdm import tqdm_notebook
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
from sklearn.metrics import roc_auc_score, roc_curve
from sklearn.model_selection import StratifiedKFold
warnings.filterwarnings('ignore')

1.データ読み込み

import feather as ftr
train = ftr.read_dataframe("../data/input/train.feather")
test = ftr.read_dataframe("../data/input/test.feather")

read_csvより早いので、feather形式で読み込みます。 こちらを参考にさせていただきました↓
Kaggleで使えるFeather形式を利用した特徴量管理法 - 天色グラフィティ

2.データの大きさ、最初の数行確認

print(train.shape)
train.head()

3.各列の欠損値を確認

def missing_data(data):
    #欠損値を含む行の合計数
    total = data.isnull().sum()
    #欠損値を含む行の合計(%表示)
    percent = (data.isnull().sum()/data.isnull().count()*100)
    tt = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'])
    types = []
    for col in data.columns:
        dtype = str(data[col].dtype)
        types.append(dtype)
    #各列の型を確認
    tt['Types'] = types
    #行列を入れ替える

missing_data(train)

結果↓
f:id:zerebom:20190412193310p:plain
欠損値は全くなく、すべての特徴量がfloat64です。

4.列ごとの統計量の確認

train.describe()

結果
f:id:zerebom:20190412193812p:plain

target=1が全体の10%ほどという、不均衡データであることを確認しておきましょう。

5.各特徴量のtarget==0の行,target==1の行の分布比較

def plot_feature_distribution(df1, df2, label1, label2, features):
    i = 0
    sns.set_style('whitegrid')
    plt.figure()
    fig, ax = plt.subplots(10,10,figsize=(18,22))

    for feature in features:
        i += 1
        plt.subplot(10,10,i)
        sns.kdeplot(df1[feature], bw=0.5,label=label1)
        sns.kdeplot(df2[feature], bw=0.5,label=label2)
        plt.xlabel(feature, fontsize=9)
        locs, labels = plt.xticks()
        plt.tick_params(axis='x', which='major', labelsize=6, pad=-6)
        plt.tick_params(axis='y', which='major', labelsize=6)
    plt.show();

t0 = train.loc[train['target'] == 0]
t1 = train.loc[train['target'] == 1]
features = train.columns.values[2:102]
plot_feature_distribution(t0, t1, '0', '1', features)

結果↓
f:id:zerebom:20190412194118p:plain

こちらで使っているseabornのkdeplotはカーネル密度推定と呼ばれるものです。
分布の密度関数推定に使われる、ノンパラメトリック推定の一種です。
簡単に説明すると、ヒストグラムと同じような分布の推定方法だけど、滑らかになるように操作を加えています(雑)
詳しくはこちらなどを参考にしてください。
http://www.ocw.titech.ac.jp/index.php?module=General&action=DownLoad&file=200927244-21-0-19.pdf&type=cal&JWC=200927244

6.trainとtestデータの各行の平均値の分布を比較

plt.figure(figsize=(16,6))
features = train_df.columns.values[2:202]
plt.title("Distribution of mean values per row in the train and test set")
sns.distplot(train_df[features].mean(axis=1),color="green", kde=True,bins=120, label='train')
sns.distplot(test_df[features].mean(axis=1),color="blue", kde=True,bins=120, label='test')
plt.legend()
plt.show()

結果↓
f:id:zerebom:20190412200923p:plain

train,testの分布がほとんど同じということが分かります。
.mean(axis=1)の部分を.mean(axis=0)にすれば列の平均値を集約し、分布の推定が行われます。
他にも、std,min,max,skew,kurtosisなどとすることで、標準偏差、最小値、最大値、尖度、歪度などの分布も確認できます。

train,とtestの分布が異なる場合は、適切にバリデーションを行わないと、
過学習を起こしてしまうので要注意です。

u++さんの記事が非常にわかりやすいのでよかったら参考にしてください。
upura.hatenablog.com

これを応用して、trainデータをtarget==0,target==1で分けて同様に分布を確認することが出来ます。

#分け方↓
t0 = train.loc[train['target'] == 0]
t1 = train.loc[train['target'] == 1]

7.重複の存在する特徴量の確認

features = train.columns.values[2:202]
unique_max_train = []
unique_max_test = []
for feature in features:
    values = train[feature].value_counts()
    unique_max_train.append([feature, values.max(), values.idxmax()])
    values = test[feature].value_counts()
    unique_max_test.append([feature, values.max(), values.idxmax()])

np.transpose((pd.DataFrame(unique_max_train, columns=['Feature', 'Max duplicates', 'Value'])).\
            sort_values(by = 'Max duplicates', ascending=False).head(15))

結果↓
f:id:zerebom:20190412202108p:plain

これが非常に大切だったのです...(のちに紹介)

8.各列ごとの相関係数確認

#各列の相関係数を求め、絶対値を取り、行列を1列に直し、相関係数の大きさで並べ替え、indexを再度付与
correlations = train[features].corr().abs().unstack().sort_values(kind="quicksort").reset_index()
#同じ列同士の行は消去
correlations = correlations[correlations['level_0'] != correlations['level_1']]
correlations.head(10)

結果↓
f:id:zerebom:20190412202851p:plain

非常に相関係数が小さいことが分かります。
このことから、それぞれの特徴量は独立に扱ってよいという考えに至れると、
新たな学習方法を行うことが出来ます。

9.各特徴量毎のtarget==0,target==1の散布図。

train.loc[train['target']==0]['var_180'].plot(figsize=(30,30),style='.')
train.loc[train['target']==1]['var_180'].plot(figsize=(30,30),style='.')

結果↓
f:id:zerebom:20190412203426p:plain

このKernelを参考にしました。
In Search of Weirdness | Kaggle

横軸が行番号、縦列が各要素の値です。 青がtarget==0,オレンジがtarget==1です。

自分は各特徴量毎にこの散布図を作成し、なめるように見ていました。
ここでは、0.48付近にtarget==1が多く含まれていて、効きそうな特徴量が作れそうですが、
この図だけでは青がどれだけ密集しているか詳しくはわからないので要注意です。

手法5で確認を行えばよいですが、

plt.figure(figsize=(30,30))
sns.kdeplot(train.loc[train['target']==1]['var_108'], bw=0.2)
sns.kdeplot(train.loc[train['target']==0]['var_108'], bw=0.2)

とすることで、特徴量毎に大きく表示し、確認することもできます。
f:id:zerebom:20190412204322p:plain

応用編(Santanderで役に立つEDA)

それぞれ長くなりそうなので、別の記事にしました。

  1. 各特徴量を独立と仮定した修正ナイーブベイズ法によるtarget確率分布推定

初めて、本気で参加したコンペについての感想

Santanderコンペについて

欠損値もなく、データも大きくなく、すべて数値型だったので取り組みやすい!
と思って始めたのですが、実際は人と差をつけることが難しく、スコアも伸びず大変でした…

LBスコアが非常に密集していて、
kernelで公開されている、0.901スコア代の人が3000人くらいいたのが特徴的でした。

自分の足りなかった部分

  • 着手し始めるのが遅すぎた。
  • NNライブラリを扱えず、アンサンブルができなかった
  • モデルの理解度が低く、LGBMでは効かない特徴量を作ってしまった。
  • 英語力が足りず、discussionから必要な情報を収集しきれなかった。
  • discussionに書いてあることをデータにうまく落とし込めなかった。
    EX)各列が無相関→各列を縦に連結し、(4000万行,2列)のデータセットにするなど

参加して得られたこと

  • Kaggleにしっかり参加するときの流れがつかめた。

u++さんが公開してくださった、ディレクトリを用いて
デバッグ、特徴量の追加削除を自由にできる環境でのKaggleを行うことが出来るようになった。

upura.hatenablog.com

  • のちのコンペに生かせそうな関数を知れた
    EDA等、dfを引数にした汎用的な関数をいくつか知ることが出来たので、今後のコンペに対してスムーズに動生きだせそう。

  • やるき
    何の成果も得られず悔しかったので、次はやってやるぞという気持ちになれた。

   

機械学習を基礎から見直すためにPRML(パターン認識と機械学習)の学習を始めてみた話

 

f:id:zerebom:20190115220651j:plain


こんばんは、ひぐです。

 卒論と学会論文とインターン(楽しい)のプロジェクトがかぶって、泡吹いています

 

今日はPRMLをやっていくぞという話をします。

 

難しい難しいとさんざん言われていますが、

数学苦手な自分なりに取り組み方がすこしわかってきたので、他の人の参考になればなと思います。

 まだ、やっている途中なので間違った表記や表現が含まれる可能性があります。(T_T)

 

自分のスペック

・地方国立大4年

・入学後は3年生の秋までサークル漬けでした

・学部はゴリゴリの理系(工学・情報系)だったので大学の

 線形、微積分、確率はやったことはある(一夜漬けばっかでしたが)

・数学は結構苦手(センター数Ⅱ54点…)

・いろいろな参考書を買うけど、どれも味見程度(はじパタ、0からDL、達人サイエンティストによる理論と実践などなど…)

 

PRMLってなに?

これです。下巻もあります。

 

割と網羅的に理論を追って機械学習について根っこから解説されており、

名著と呼ぶ方も多くいらっしゃいます。

 

ただ、線形代数、確率論、微積分などの計算量がかなり多く、挫折者を多く生んでいるとか汗

なんで始めようと思ったの?

 

自分は5月ごろから機械学習の勉強を始めました。

 今まではいろいろな参考書やQiitaをちょっとずつかじったり、

kaggleをすこーしだけやって勉強していました。

 

知識が飛び飛びで難しいモデルに対してはよくわかんないけどとりあえずコードコピペしちゃえ!

みたいな感じだったのです。

 

しかしこれではいつまでたっても本質的な力はつかないなと思い腹をくくって始めました。

 

あとは、自分は勉強をかなり適当に済ませて、やった気になる癖があるので

2019年はそういうのをなくしていきたい!と思い、重めの本を腰を据えてやることにしました。

 

 

 

どうやって進めているの?

今の自分だとこの本だけだと、かなり厳しいので随所調べながら進めています。

仮にも理系の4年生だというのに出来ないのは悲しいですが、今までのツケを支払っていきたいと思います;;

 

具体的には一つずつ数式を追っていき、理解できない式展開が行われた場合は手計算をしてみるといった感じです。

1ページに30~50分くらいかかるときもあります。

 

有名な本で勉強会が多く開かれているため、slideshareなどの資料を参考にすると良いと思います。

 

また、過去にこの本を読み進めるにあたって必要な数学の知識をPDFに

まとめてくださったかたがいらっしゃいます。(神)

 

こちらも参考にしてみてはいかがでしょうか。

パターン認識機械学習の学習という名前ですw)

d.hatena.ne.jp

 

また、PRMLを進める前にベイズについて勉強しといたほうがいいよといわれたので、事前にこの本でベイズ統計については学んでいました。

 

この本はかなり丁寧なのでおすすめです。僕的には結構難しかったです。

 

 

 

現在の進捗

1章をサラッと進め、2章をじっくりやっています。

2.3くらいまでは終わりました。

 

今年度中には終わらせたいです。

 

感想

確かに難しいですが、きっちりやればかなり力がつくと思います。

 

そして今まで何となくの理解だった部分が腑に落ちた!

ってなる部分がいっぱいあります。

 

例えば、

多次元曲線で過学習が起きるのは各係数が非常に大きくなるからとか、

ガウス分布最尤推定は、MSEの最小化と一致するとか

ベータ分布は二項分布の事前分布として導入するとすっきりする、とか…!

(↑このあたり間違ってたらごめんなさい)

 

kaggleや最新の研究では主流じゃないようなすこし古いモデルや、手法も出てくるので

効率的でないと思った節もあったのですが、

 

この本の意義としては、

機械学習、深層学習で必要になってくる数学の知識と理論を一定水準まで引き上げられる。

 

ことだと思います。ですので、式展開とかもちゃんと追っていきたいです。

この本が読み終われば、大抵の論文の式変形などにウッってならなくなりそうです。

 

 

 

 

 

 

まとめ

 

サクッっと書かせていただきました。

 研究室の先生にはいつも

「わかるとは『人にわかりやすく説明できる』こと」

と言われているので、とりあえず流して終わったぞ~ってならない様にブログで

アウトプットできたらなと思います💦

 

そんなこんなでやっていくぞという気持ちです、途中でやめない様に記事にしました。

一緒に勉強してくれる人も募集しております。

 

では~

 

 

 

 

 

 

 

windowsでpythonから管理者権限が必要なことを実行する方法

こんにちはひぐです!

 

きょうはニッチな情報なんですが、

windowspythonを使って管理者権限が必要なことを実行する方法を紹介します^^

 

 

動機

pythonからIPアドレスを変更する方法したかったのですが、

要求された操作には、権限の昇格が必要です。管理者として実行してください。

と出てしまう

 

windowsでは、管理者としてログインしていたとしてもネットワークの設定を変えるには認証がいるようです。

 

今回の記事では、IPアドレスを変更する方法を例にタイトルの内容を紹介していきたいと思いますm(__)m

 

GUIで変更する方法

普通の使い方をする人はこれで大丈夫だと思います。

そして非常によくまとまっていた記事があるので、丸投げします()

pc-chain.com

 

コマンドプロンプトIPアドレスを変更する方法

 

まずはコマンドプロンプトを管理者権限で開く(右クリックより選択)。もしくは、

powershell start-process cmd -verb runas

と打つ。

参考:コマンドプロンプトから管理者権限のコマンドプロンプトに切り替える - Qiita

 

これは、

powershellを起動し、コマンドプロンプト(cmd)を管理者権限(ruans)で

  実行する(start-processes)」

という意味になります。

 

そうすると、管理者と書かれたコマンドプロンプトが別に開くので、

 

netsh interface ip set address "接続名" static xxx.xxx.xxx.xxx(IPアドレス) xxx.xxx.xxx.xxx(サブネットマスク) xxx.xxx.xxx.xxx(デフォルトゲートウェイ) 

 

と入力します。

参考:[メモ][Windows] コマンドラインでIPアドレスを変更する | Nobwak's Lair

 

 

サブネットマスク/デフォルトゲートウェイとはなんぞ?

という方のためにリンクを貼っておきます。↓

サブネットマスク(subnet mask)とは?

デフォルトゲートウェイって何? |【エン転職】

 

そして、コマンドプロンプト内で

ipconfig /all 

 と打って、IPアドレスが変更されているのを確認します。

 

 

pythonIPアドレスを変更する方法

前置きが長くなりました、本題に移ります!

pythonコマンドプロンプトのコードを実行するにはsubprocessを使います。

import subprocess
cmd='hogehoge'
subprocess.call(cmd,shell=True)

とすればOKです。

参考:17.5. subprocess — サブプロセス管理 — Python 3.3.6 ドキュメント

 

しかし、pythonで'powershell start-process cmd -verb runas'を打ったとしても

別ウィンドウが開くだけでsubprocessでの操作が管理者権限になるわけはないです涙。

 

ここではかなり頭を悩まされましたが、

バッチファイルを作成しそれを起動させるということにしました。

 

※バッチファイル…クリックするとそのコードをコマンドプロンプトで実行するファイル。

         メモ帳などで書き、拡張子を.batにすれば完成する。

 

参考:必要に迫られてバッチファイルを組むことになった人向けのTips - Qiita

 

 

参考になったのは以下の記事です!

piyopiyocs.blog115.fc2.com

 

具体的な方針

  1. 管理者権限でコマンドプロンプトを開き○○というコードを実行するという
    バッチファイルAを作る
  2. 実行したい○○というコードが書かれたバッチファイルBを作成する
  3. pythonからバッチファイルAを実行するという指示を飛ばし、
    本来の目的であるバッチファイルBの内容を実行する

 

バッチファイルAの中身

powershell.exe -Command Start-Process """%1""" "%2" -Verb Runas

"""%1""" "%2"は引数です。%1は実行したいバッチファイルBのパス、%2はバッチファイルBに渡す引数です

-------------------------------------------------------------------------------------------------------------------

 

 

バッチファイルBの中身

@echo off
netsh interface ip set address "イーサネット" static xxx.xxx.xxx.%1(IPアドレス) xxx.xxx.xxx.xxx(サブネットマスク) xxx.xxx.xxx.xxx(デフォルトゲートウェイ) 

 

@echo offは実行したときに、コマンドプロンプトでコードを表示しないという指示になります。

今回はIPアドレスの最終桁を変更したかったので%1(バッチファイルAからみると%2)を渡しました。

-------------------------------------------------------------------------------------------------------------------

 

 

 

そして、pythonには以下のように記述をします

cmd4=fr'バッチファイルAのパス バッチファイルBのパス {IPアドレス下1桁(int)}'
    subprocess.call(cmd4,shell=True)

 

これでOKです!

 

役に立つ人がいるか微妙な記事ですが、

3日前の俺からしたらのどから手が出るほど欲しい記事なので書きました笑

 

たぶんmacならsudoで一発なんだろうなぁ(涙)

 

めちゃくちゃネットワーク周りには疎かったのですが、

紆余曲折したおかげでちょっとだけTCP/IP周りに詳しくなりましたw

 

もっといい方法ありそうなので、もし知っている方がいたらコメントしていただけたら幸いです!

 

 

では~

統計検定2級に受かったので勉強法等色々纏めてみた

お久しぶりです、ひぐです。

今回は統計検定2級についての記事です!

 

大体1か月半くらいで取得することが出来ました。

将来データサイエンティストになりたいな、

と最近ぼんやり考えてきたので勉強を始めたといった感じです。

 

資格を取ることは初めてだった(英検4級だけもってる)のですが、

統計検定はネットから気軽に応募できる上に良問といった感じで、

とても受験してよかったなと思っています!

 

 

今回の記事では 以下のようなことを紹介したいと思います↓

 

 

統計検定何が魅力なの??

↑これが一番伝えたいので、最初に持ってきます!

まず、個人的に統計ってのはめちゃくちゃ大事な学問だと思っています!

www.diamond.co.jp

 

昔はデータを集める、保管しておく、扱うということは凄く難しいことでしたが

WEBが発達した今はビックデータを集めることが出来るようになりました。

統計はデータが多いほど真価を発揮する学問なのでこれからの時代、これまで以上に、役立つかと思います。

 

最近はやりの機械学習/人工知能統計学がもとになりたっていますから!

(語弊あるかも)

 

そして、2級まではCBT方式といって全国各地で受験できる上に、

日程を選ぶことが出来ます!

なので、やる気が出たら勉強→即受験なんてかんじに気軽に受けられます!

 

さぁ、みんなもLet`s統計!

(回し者じゃないです)

 

 

統計検定とは?

 

公式サイトはこちら

www.toukei-kentei.jp

統計検定は

 

「統計検定」とは、統計に関する知識や活用力を評価する全国統一試験です。
データに基づいて客観的に判断し、科学的に問題を解決する能力は、仕事や研究をするための21世紀型スキルとして国際社会で広く認められています。
日本統計学会は、中高生・大学生・職業人を対象に、各レベルに応じて体系的に国際通用性のある統計活用能力評価システムを研究開発し、統計検定として実施します

 

とのこと。日本統計学会が実施している全国統一模試です。

 

みんな受けてるの?

これくらいの人が受けているそうです。↓

4級~1級の累計

統計検定の記録(分布)|統計検定:Japan Statistical Society Certificate

f:id:zerebom:20181214230348p:plain

 

 

大体毎年5000人くらい受けているそうです。

最近はCBT方式も開始されたので、

このグラフよりは増えているかと思います。

(このグラフはおそらく、定期開催で受験した人だけ)

 

ちなみに、基本情報技術者試験は年間10万人程度受けているようです…!

統計情報|基本情報技術者試験.com

受けた身としてはこれくらいメジャーになったらいいなぁって感じですw

 

2級ってどのくらいの難しさなの?

統計検定とは|統計検定:Japan Statistical Society Certificate

f:id:zerebom:20181214231206p:plain

 だいたいこんな感じです。

 

2級は理系の学生なら比較的取りやすいけど、

数学から離れていた人だとわりと勉強しないといけないかなって感じです!

 

見た感じ、

1級>>>準1級>>>>>>>>>>2級>>3級>4級

って感じがしました。(笑)

 

 

 

数学の知識は数2B あたりまでがしっかりわかっているといいかなと思います。

最低限

  • 平方根・乗数・対数の意味、計算方法
  • 簡単な微積
  • 条件付き確率、組み合わせの計算

などが身についていれば後述する統計WEBを見ても???ってならずに進められると思います。

 

 

どうやって勉強したらいいの?

 

自分は

  1. (全体把握)統計WEBを暇な時間に流し読みで1週
  2. (詳細理解)統計WEBで詰まったところをノートにまとまる
  3. (実践になれる)過去問を解く
  4. (苦手を埋める)わかんないとこをノートにまとめる
  5. (残りは気合で暗記)ノートと統計WEBをずっと見る

 

って感じで進めました

 

統計WEBとは?

bellcurve.jp

 

なんと、こちらのサイトで統計検定2級までの範囲は全て網羅することが出来ます!

累計200ページ以上にわたって詳細に紹介しているので、

まずはこのサイトを流し読みすることをお勧めします。

 

 

その後過去問を解きましょう

 

www.toukei-kentei.jp

 

受けた時の感想

受かるのはめっちゃ簡単?

まず、統計検定は60点が合格ラインなのですが、

60点は比較的簡単に取ることが出来るかと思います。

 

なぜならば、

  • 五択問題なので半分しかわからなくても残りは当てずっぽうで6割に届く
  • 簡単な問題とそうでない問題がはっきり分かれているので、簡単な問題だけミスらなければ難しい範囲(分散分析/重回帰分析等)がわかってなくても受かる

 

からです!

 

つまり80点取るのと、60点取るのでは難易度がかなり違います。

 

 

今後も1級を目指したり研究/業務で使うつもりの人は高得点を狙うことを目標に

したほうが良いと思います(とか言って僕は70点でした

 

CBTと普通の問題の質が微妙に違う

これは受けてから気づいたのですが、

 

普通の試験

  • 大問8~10こ×小問3~4こずつ

みたいな形式ですが、

 

CBT試験は

  • 小問30こ+大問2こ×小問2こ

みたいな形式でした!

 

なので、通常の過去問で勉強した人はCBTで受ける際にCBTの過去問を少し見ておくと面を食らわないかなと思います。

 

出題方法の性質上、CBTの方が論理より知識を問う感じがしました。

  • 2標本の母平均・母分散の95%信頼区間の求め方
  • さまざまな確率分布の式

などがたくさん出てきました。

 

その他CBT試験の豆知識
  • 時計を忘れたのですが、左下に書いてありました。(助かった)
  • 油性のマッキーとプレート2枚を渡され、計算用紙として使いました
  • 予約してから試験までには1週間のスパンが必要&試験5日前には振り込む必要がある
  • 合格後、合格証がその場で印刷してもらえる(各分野の正答率も書いてある)

 

 

以上です。

こんな感じです、わかんないことがあったらTwitterでも聞いてくれればと思います!

 

では~

 

 

 

 

 

 

Amazonの合計利用金額を自動で算出する方法

f:id:zerebom:20181110102422p:plain

こんにちは、ひぐです!

 

突然ですが、Amazonの商品購入のスムーズさってやばいですよね。

欲しいと思ってるうちに手が滑って、気づいたら家に届いてるんすよね。

 

Amazonの滑らかなUIによって、

僕の財布の紐はひもQ並みにゆるゆるになってしまいました。

 

今回はAmazonでの使いすぎを防ぐべく、

Pythonを用いて「Amazonの合計使用金額を算出」する方法を紹介します。

使用金額を確認して無駄遣いを減らしていきましょう:D

 

この記事はこの本を参考にしました

gihyo.jp

 

 

 

コード全文

import sys
import os
import re
from robobrowser import RoboBrowser

# 認証情報は環境変数から取得する。
AMAZON_EMAIL = os.environ['AMAZON_EMAIL']
AMAZON_PASSWORD = os.environ['AMAZON_PASSWORD']

# RoboBrowserオブジェクトを作成する。
browser = RoboBrowser(
    parser='html.parser',  # Beautiful Soupで使用するパーサーを指定する。
    # Cookieが使用できないと表示されてログインできない問題を回避するため、
    # 通常のブラウザーのUser-Agent(ここではFirefoxのもの)を使う。
    user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecko/20100101 Firefox/45.0')


def main():
    # 注文履歴のページを開く。
    sum_money=0
    print('Navigating...', file=sys.stderr)
    browser.open('https://www.amazon.co.jp/gp/css/order-history')

    # サインインページにリダイレクトされていることを確認する。
    assert 'Amazonログイン' in browser.parsed.title.string

    # name="signIn" というサインインフォームを埋める。
    # フォームのname属性の値はブラウザーの開発者ツールで確認できる。
    form = browser.get_form(attrs={'name': 'signIn'})
    form['email'] = AMAZON_EMAIL  # name="email" という入力ボックスを埋める。
    form['password'] = AMAZON_PASSWORD  # name="password" という入力ボックスを埋める。

    # フォームを送信する。正常にログインするにはRefererヘッダーとAccept-Languageヘッダーが必要。
    print('Signing in...', file=sys.stderr)
    browser.submit_form(form, headers={
        'Referer': browser.url,
        'Accept-Language': 'ja,en-US;q=0.7,en;q=0.3',
    })

    # ページャーをたどる。
    while True:
        assert '注文履歴' in browser.parsed.title.string  # 注文履歴画面が表示されていることを確認する。
        print(browser.parsed.title.string)

        sum_money+=print_order_history()  

        link_to_next = browser.get_link('次へ')  # 「次へ」というテキストを持つリンクを取得する。
        if not link_to_next:
            print('直近半年のAmazon使用合計金額は')
            print(str(sum)+'円です')
            break  # 「次へ」のリンクがない場合はループを抜けて終了する。

        print('Following link to next page...', file=sys.stderr)
        browser.follow_link(link_to_next)  # 「次へ」というリンクをたどる。


def print_order_history():
    """
    現在のページのすべての注文履歴を表示する。
    """
    # ページ内のすべての注文履歴について反復する。ブラウザーの開発者ツールでclass属性の値を確認できる。
    sum=0
    for line_item in browser.select('.order-info'):
        order = {}  # 注文の情報を格納するためのdict。
        # 注文の情報のすべての列について反復する。
        for column in line_item.select('.a-column'):
            label_element = column.select_one('.label')
            value_element = column.select_one('.value')
            # ラベルと値がない列は無視する。
            if label_element and value_element:
                label = label_element.get_text().strip()
                value = value_element.get_text().strip()
                order[label] = value  # 注文の情報を格納する。

        print(order['注文日'], order['合計'])  # 注文の情報を表示する。
        price=re.sub(r'¥ ' ,'',order['合計'])
        price=re.sub(',' ,'',price)


        sum+=int(price)
    return(sum)

if __name__ == '__main__':
    main()

 

 

コードの概要

Robobrowserというライブラリを用いてブラウザを開き、

フォームに情報を入力し、ログインする。

その後、値段が記載されているCSSの特定の要素を選択し、

for文を用いてページ内の商品を全て取得し合計を出力する。

 

細かい説明

 

RoboBrowserってなに?

RoboBrowserはヘッドレスブラウザ(画面が表示されないブラウザ)です。

ブラウザの動きをコードで指定し、自動で動かしてくれるライブラリです。

ヘッドレスブラウザはいくつもあるのですが、robobrowserは

フォームの入力など簡単にできます

GitHub - jmcarp/robobrowser

 

ちなみに、RoboBrowserでCSSタグを取得するときは、

HTML構文解析ライブラリのBeautiful Soupと同じ記法で出来ます。

kondou.com - Beautiful Soup 4.2.0 Doc. 日本語訳 (2013-11-19最終更新)

 

os.environってなに?

今回のコードはAmazonにログインするため、メアドとPWが必須になります。

コードに直接個人情報を書き込むのは危険なため、os.environによってローカルの別ファイルに保存してある文字列を読み込んで、実行します。

 

詳しくは

note.nkmk.me

 

実行結果

f:id:zerebom:20181110102200p:plain

 

使いすぎワロタ

 

おわり

google-site-verification: google1c6f931fc8723fac.html