カテゴリカルデータからダミー変数を作成するR関数を自作(複数列対応可)(CRANに登録)

以前、 このページ でカテゴリカルデータからダミー変数を作成するR関数を紹介し、 GitHub で公開しました。関数が1つだけの非常に簡単なものです。

時々検索されているようなので、英語に不安はありますが思い切ってCRANに投稿してみました。

対応して頂いたのはUwe Ligges氏でした。「パッケージの説明文(Description)が理解しにくい」と指摘を受けたため、少し改善して再投稿したところアクセプトされました。

パッケージの説明は このページ にあります。

Rからは

install.packages("makedummies")

でインストールできます。

使用方法はGitHubの例を参照して下さい。

カスタムメニューの内容をpecoで選択するためのスクリプトを作成

経緯

以前はfluxboxを使用しており、右クリックでメニューが出てきました。そのメニューはファイルを書き換えることでカスタマイズできました。

現在xmonadに移行しており、いくつかのキーにアプリケーションを割り当てています。

割り当てるアプリケーションが多くなると覚えるのが大変なので、カスタマイズできるメニューがないかと探していたのですが見つかりませんでした(探し方が悪いため?)。

そこで自分でスクリプトを書きました。慣れていないので多少時間はかかりました。

内容

Rubyスクリプト

GitHub で公開しています。

#!/usr/bin/env ruby
# coding: utf-8

# peco binary
PECO = File.expand_path "/usr/bin/peco"

# terminal emulator (xterm/urxvt/mlterm)
XTERM = "mlterm"

# menu list
menufile = File.expand_path "~/.mymenu"


require 'tempfile'

class MyMenu
  def initialize(menufile)
    @menufile = menufile
    @appfilepath = Tempfile.new("tmpnew").path
    @hash = Hash.new
  end

  protected

  def set_hash_and_list
    io1 = File.open(@menufile, "r")     # filelist
    io2 = File.open(@appfilepath, "w")  # list of application name

    io1.each {|line|
      line.strip!
      line.chomp!
      next if line == "" || (/^#/ =~ line)

      rows = line.split(/,/)
      rows[1] = rows[0] if rows[1].nil?
      @hash.store(rows[0], rows[1])
      io2.puts rows[0]
    }
    io1.close
    io2.close
  end

  def exec_peco
    out = Tempfile.new("peco-out")
    err = Tempfile.new("peco-err")
    system "#{XTERM} -e sh -c '#{PECO} #{@appfilepath} > #{out.path} 2> #{err.path}'"

    res = `cat #{out.path}`
    if res != ""
      app = @hash[res.chomp!]
      system("#{app} &")
    end
  end

  public

  def exec_MyMenu
    self.set_hash_and_list
    self.exec_peco
  end
end


mymenu = MyMenu.new(menufile)
mymenu.exec_MyMenu

このスクリプトの後半はるびきち様が公開している peco を参考にさせて頂きました。というよりもそのまま使用させて頂きました。

設定

mymenu.rb

XTERM に使用したいターミナルエミュレーターを指定します。

  • xterm/urxvt/mlterm であれば大丈夫でした。
  • gnome-terminal/lxterminal は失敗しました。

~/.mymenu

~/.mymenu を作成し、以下のように記載します。 このファイルが存在しないとエラーが出ます。

FD, gnome-terminal -e fd
R, lxterminal -e R
leafpad

メニューに表示したい内容と実行するコマンドをカンマで区切って記載するだけです。

  • カンマの左側に記載した内容がメニューに表示されます。
  • カンマの右側に記載した内容が実行されます。
    • 省略した場合には左側の内容がそのままコマンドとして実行されます。
    • ターミナルで実行したい場合には “(使用したいターミナル) -e (実行したいコマンド)” のように記載します。

使用法

mymenu.rbを好きなキーに割り当てます。

あとは好きなように絞り込みをするだけです。

Hit-a-Hint の設定をしておけば、「@」を押した後に候補の左端に表示されている文字を打ち込むことで即座に選択できるようになります。

カテゴリカルデータからダミー変数を作成するR関数を自作(複数列対応可)(その後)

追記(2017.1.3)

makedummiesパッケージがCRANに登録されました。ブログの このページ を参照して下さい。

内容

以前、 このページ でカテゴリカルデータからダミー変数を作成するR関数を紹介しました。

このブログ自体それほどアクセスがあるわけではありませんが、検索される際のキーワードとして「R ダミー変数」が最も多かったです。

そこでこの関数をGitHubで公開しました(https://github.com/toshi-ara/makedummies)。

Rからは

## install.packages("devtools") # パッケージをインストールしていない場合
library(devtools)
install_github("toshi-ara/makedummies")

でインストールできます。

使用方法はGitHubの例を参照して下さい。

カテゴリカルデータからダミー変数を作成するR関数を自作(複数列対応可)

追記(2017.1.3)

makedummiesパッケージがCRANに登録されました。ブログの このページ を参照して下さい。

追記(2015.12.26)

GitHubで関数を公開しました。ブログの このページ を参照して下さい。

経緯

Rのglm関数を用いて重回帰分析を行う際には、カテゴリカルデータを自動的にダミー変数に変換してくれるため、あまり苦労しません。

現在、個人的にRStanを勉強中であり、同じ解析を行うためには自分でダミー変数を作成する必要があります。さらにカテゴリカルデータと数値データが混在しているとそれだけで非常に大変です。

そこで群馬大学の青木先生が公開されているmake.dummy関数( http://aoki2.si.gunma-u.ac.jp/R/src/make.dummy.R )をもとにして関数を作成しました。

作成した関数

(2015.9.6追記)以下のプログラムを修正したものを記事の下に掲載しています。こちらを使用せずにそちらを参考にして下さい。

# http://aoki2.si.gunma-u.ac.jp/R/src/make.dummy.R
make.dummy <- function(dat, basal_level= FALSE, sep = "_") {
    name <- colnames(dat)
    level <- levels(dat[,1])
    if (!is.data.frame(dat))
        dat <- as.data.frame(dat)
    ncat <- ncol(dat)
    dat[, 1:ncat] <- lapply(dat, function(x) {
        if (is.factor(x)) {
            return(as.integer(x))
        } else {
            return(x)
        }
    })
    mx <- sapply(dat, max)
    start <- c(0, cumsum(mx)[1:(ncat-1)])
    nobe <- sum(mx)
    ## modified
    res <- t(apply(dat, 1, function(obs) 1:nobe %in% (start+obs))) + 0
    colnames(res) <- paste(name, level, sep = sep)
    if (basal_level == FALSE) res <- res[,-1]
    return(res)
}


make.dummys <- function(dat, ...) {
    n <- ncol(dat)
    res_list <- lapply(seq(n), function(i) {
        tmp <- as.data.frame(dat[,i])
        colnames(tmp) <- colnames(dat)[i]
        if (is.factor(dat[,i])) {       # factor or ordered
            make.dummy(tmp, ...)
        } else {
            tmp
        }
    })

    res <- NULL
    for (i in seq(n)) {
        res <- cbind(res, res_list[[i]])
    }
    return(res)
}

解説

make.dummy関数

青木先生が公開されているmake.dummy関数を少しだけ改変しました。データはデータフレーム形式で渡します。

変更点は以下の点です。

  1. 基準となるカテゴリーを削除できるように変更
    • basal_level引数が FALSE => 基準となるカテゴリーを削除する(デフォルト)
    • basal_level引数が TRUE => 基準となるカテゴリーを削除しない
  2. 列名を追加
    • sep引数で変数名とカテゴリー名を連結する文字列を設定(デフォルトは”_”)

(2015.9.6追記)新バージョンではこの関数は削除しました。ただし、引数の説明はこのまま使用できます。

make.dummys関数

元データが複数の列からなっている場合にも一度にダミー変数に変更するようにしました。

また、カテゴリカルデータ以外(主に数値データ)の場合にはそのまま出力するようにしました。

ですので make.dummys関数を使用すればOKです。引数sepとbasal_levelも使用可能です。

使用例

カテゴリカルデータの場合

基準となるカテゴリーを削除する場合

dat <- data.frame(x = factor(rep(c("a", "b", "c"), each = 3)))
dat$x
make.dummys(dat)
[1] a a a b b b c c c
Levels: a b c

      x_b x_c
 [1,]   0   0
 [2,]   0   0
 [3,]   0   0
 [4,]   1   0
 [5,]   1   0
 [6,]   1   0
 [7,]   0   1
 [8,]   0   1
 [9,]   0   1

基準となるカテゴリーを削除しない場合

make.dummys(dat, basal_level = TRUE)
     x_a x_b x_c
[1,]   1   0   0
[2,]   1   0   0
[3,]   1   0   0
[4,]   0   1   0
[5,]   0   1   0
[6,]   0   1   0
[7,]   0   0   1
[8,]   0   0   1
[9,]   0   0   1

変数名とカテゴリー名を連結する文字列を変更

make.dummys(dat, sep = ":")
     x:b x:c
[1,]   0   0
[2,]   0   0
[3,]   0   0
[4,]   1   0
[5,]   1   0
[6,]   1   0
[7,]   0   1
[8,]   0   1
[9,]   0   1

順序のあるカテゴリカルデータの場合

dat <- data.frame(x = factor(rep(c("a", "b", "c"), each = 3)))
dat$x <- ordered(dat$x, levels = c("a" ,"c" ,"b"))
dat$x
make.dummys(dat)
[1] a a a b b b c c c
Levels: a < c < b

      x_c x_b
 [1,]   0   0
 [2,]   0   0
 [3,]   0   0
 [4,]   0   1
 [5,]   0   1
 [6,]   0   1
 [7,]   1   0
 [8,]   1   0
 [9,]   1   0

カテゴリカル変数は意味のある語が使用されることが多いため、実際にはordered変数を使用して順序のあるカテゴリカルデータとして扱うことが多いと思います。

数値データの場合

dat <- data.frame(x = rep(1:3, each = 3))
dat$x
make.dummys(dat)
  x
1 1
2 1
3 1
4 2
5 2
6 2
7 3
8 3
9 3

数値データはそのまま出力されます。

複数の列をもつ場合

2つのカテゴリカルデータ

dat <- data.frame(
    x = factor(rep(c("a", "b", "c"), each = 3)),
    y = factor(rep(1:3, each = 3))
)
make.dummys(dat)
     x_b x_c y_2 y_3
[1,]   0   0   0   0
[2,]   0   0   0   0
[3,]   0   0   0   0
[4,]   1   0   1   0
[5,]   1   0   1   0
[6,]   1   0   1   0
[7,]   0   1   0   1
[8,]   0   1   0   1
[9,]   0   1   0   1

それぞれダミー変数として出力されます。

カテゴリカルデータと数値データ

dat <- data.frame(
    x = factor(rep(c("a", "b", "c"), each = 3)),
    y = rep(1:3, each = 3)
)
make.dummys(dat)
  x_b x_c y
1   0   0 1
2   0   0 1
3   0   0 1
4   1   0 2
5   1   0 2
6   1   0 2
7   0   1 3
8   0   1 3
9   0   1 3

カテゴリカルデータと数値データが混在してもカテゴリカルデータのみがダミー変数に変換されます。

追記

2015.9.6

プログラムを修正しました。

  • make.dummys関数の1つだけで動作するように改変しました。
  • ダミー変数に変換するプログラムも簡潔にしました。
  • 得られる結果は同じです。
  • ただし、元データの行ラベルも再現するように変更しました。

2015.9.11

プログラムを少し修正しました。

統計学関連なんでもあり のNo. 21771から始まるスレッドの青木先生の書き込みをもとに変更しました。

こちらの方が行数は増えますが、処理の内容が明確でした。

make.dummys <- function(dat, basal_level = FALSE, sep = "_") {
    n_col <- ncol(dat)
    name_col <- colnames(dat)
    name_row <- rownames(dat)

    result <- NULL
    for (i in seq(n_col)) {
        ## process each column
        tmp <- dat[,name_col[i]]
        if (is.factor(tmp)) {
            ## factor or ordered => convert dummy variables
            level <- levels(droplevels(tmp))
            ## http://aoki2.si.gunma-u.ac.jp/taygeta/statistics.cgi
            ## No. 21773
            m <- length(tmp)
            n <- length(level)
            res <- matrix(0, m, n)
            res[cbind(seq(m), tmp)] <- 1
            ## res <- sapply(level, function(j) ifelse(tmp == j, 1, 0))
            colnames(res) <- paste(name_col[i], level, sep = sep)
            if (basal_level == FALSE) {
                res <- res[,-1]
            }
        } else {
            ## non-factor or non-ordered => as-is
            res <- as.matrix(tmp)
            colnames(res) <- name_col[i]
        }
        result <- cbind(result, res)
    }
    rownames(result) <- name_row
    return(result)
}

f.elに関するメモ

Emacsのモダンなライブラリ4+1選 で紹介されていたf.elパッケージについて、少し勉強したのでその内容をメモ的に残しておきます。

このページの存在は少し前に知っていましたが、 るびきち様 にパッケージと関数を教えて頂いたので自分でも調べてみました。

ファイルに関する関数

f-で始まる関数がf.elパッケージで提供されている関数です。

関数名が短かくて分かりやすくなるとともに ~を自動で展開 (expand-file-name)してくれるものが多いため プログラムが簡潔になりそうです。

また、ファイルの拡張子を変更するf-swap-ext関数は非常に便利かもしれません。

(require 'f)
(setq file "~/.emacs.d/init.el")

;;; ファイル
;; 絶対パス(フルパス)
(expand-file-name file)          ; => "/home/ara/.emacs.d/init.el"
(f-expand file)                  ; => "/home/ara/.emacs.d/init.el"
;; ファイル名(ディレクトリ以外)
(file-name-nondirectory file)    ; => "init.el"
(file-name-nondirectory
 (directory-file-name file))     ; => "init.el"
(f-filename file)                ; => "init.el"
;; ファイルのベースネーム
(f-base file)                    ; => "init"
;; 拡張子
(file-name-extension file)       ; => "el"
(f-ext file)                     ; => "el"
;; 拡張子なしのパス
(file-name-sans-extension file)  ; => "~/.emacs.d/init"
(f-no-ext file)                  ; => "~/.emacs.d/init"

;;; ファイル => ディレクトリ(末尾の/なし)
(directory-file-name
 (file-name-directory file))     ; => "~/.emacs.d"
(f-dirname file)                 ; => "/home/ara/.emacs.d"
;;; ファイル => ディレクトリ(末尾の/あり)
(file-name-directory file)       ; => "~/.emacs.d/"
(f-slash (f-dirname file))       ; => "/home/ara/.emacs.d/"

;; パスの結合(/の有無は自動で処理される)
(f-join "~/.emacs.d" "init.el")  ; => "/home/ara/.emacs.d/init.el"
(f-join "~/.emacs.d/" "init.el") ; => "/home/ara/.emacs.d/init.el"

;; 拡張子の変更
(f-swap-ext file "elc")          ; => "~/.emacs.d/init.elc"

ディレクトリに関する関数

  • 末尾の/を外すのがf-long
    • 内部でf-expandを呼び出しているだけ(f-expandで十分かも)
  • 末尾の/をつけるのがf-full
    • f-longの後にf-slashを適用している
(require 'f)
(setq dir1 "~/.emacs.d")
(setq dir2 "~/.emacs.d/")

;;; 絶対パス(フルパス)
(expand-file-name dir1)          ; => "/home/ara/.emacs.d"
(expand-file-name dir2)          ; => "/home/ara/.emacs.d/"
(f-expand dir1)                  ; => "/home/ara/.emacs.d"
(f-expand dir2)                  ; => "/home/ara/.emacs.d"

;;; ディレクトリ
;; ディレクトリ(末尾の/なし)
(directory-file-name dir1)       ; => "~/.emacs.d"
(directory-file-name dir2)       ; => "~/.emacs.d"
(f-long dir1)                    ; => "/home/ara/.emacs.d"
(f-long dir2)                    ; => "/home/ara/.emacs.d"
;; ディレクトリ(末尾の/あり)
(file-name-as-directory dir1)    ; => "~/.emacs.d/"
(file-name-as-directory dir2)    ; => "~/.emacs.d/"
(f-slash dir1)                   ; => "~/.emacs.d/"
(f-slash dir2)                   ; => "~/.emacs.d/"
(f-full dir1)                    ; => "/home/ara/.emacs.d/"
(f-full dir2)                    ; => "/home/ara/.emacs.d/"

その他の関数

f-readあるいはf-read-textは非常に便利そうです。

with-temp-buffer => insert-file-contents のような作業を内部で行ってくれます。結果がバッファへの挿入ではなく文字列として返ってくるのだけが異なりますが、それでもプログラムが非常に簡潔になりそうです。

;; ファイルを文字列として読み込む(codingは省略可能:デフォルトは'utf-8)
(f-read path coding)
(f-read-text path coding)

;; 文字列をファイルに保存(codingは省略可能:デフォルトは'utf-8)
(f-write text coding path)
(f-write-text text coding path)

;; このファイルのフルパスを得る
(f-this-file)

メール送信スクリプト

Linuxのコマンドラインからメールを送信するスクリプトを作成(正しくはwebの内容を改変)した。

  • 添付ファイル可能(0でも複数でも)

使用する機会は今の所ほとんどないと思う……

詳しくはwikiで。

#!/bin/bash

############################
## メール添付ファイルスクリプト
## コマンドラインから添付付きメールを送信するスクリプトです.
##
## send_mail.sh {toアドレス} {タイトル} {本文ファイル} [{添付ファイル名}...]
##
############################

## Init section
from_addr='ara_t@po.mdu.ac.jp'
to_addr=$1
subject=`echo $2 | nkf -w -Lu`
mail=`cat $3 | nkf -w -Lu`
shift; shift; shift
boundary=`date +%Y%m%d%H%M%N`

## generate MIME encoded mail
echo "From: $from_addr" > .MIME_MAIL.tmp
echo "To: $to_addr" >> .MIME_MAIL.tmp
echo "Subject: $subject" >> .MIME_MAIL.tmp
echo "MIME-Version: 1.0" >> .MIME_MAIL.tmp
echo "Content-type: multipart/mixed; boundary=\"----$boundary\"" >> .MIME_MAIL.tmp
echo "Content-Transfer-Encoding: 7bit" >> .MIME_MAIL.tmp
echo "" >> .MIME_MAIL.tmp
echo "This is a multi-part message in MIME format." >> .MIME_MAIL.tmp
echo "" >> .MIME_MAIL.tmp

## Insert MAIL_BODY (if you need)
echo "------$boundary" >> .MIME_MAIL.tmp
echo "Content-type: text/plain; charset=utf-8" >> .MIME_MAIL.tmp
echo "Content-Transfer-Encoding: 7bit" >> .MIME_MAIL.tmp
echo "" >> .MIME_MAIL.tmp
echo "$mail" >> .MIME_MAIL.tmp
echo "" >> .MIME_MAIL.tmp

## Attach file to E-Mail
while [ $# -ge 1 ] ; do
  attach_file=$1
  filename=`echo $attach_file | sed -e "s/.*\/\(.*$\)/\1/"`
  content_type=`file --mime $attach_file | cut -d' ' -f2`
  echo "------$boundary" >> .MIME_MAIL.tmp
  echo "Content-type: $content_type" >> .MIME_MAIL.tmp
  echo " name=$filename" >> .MIME_MAIL.tmp
  echo "Content-Transfer-Encoding: base64" >> .MIME_MAIL.tmp
  echo "Content-Disposition : attachment;" >> .MIME_MAIL.tmp
  echo " filename=$filename" >> .MIME_MAIL.tmp
  echo "" >> .MIME_MAIL.tmp
  cat $filename | base64 >> .MIME_MAIL.tmp
  echo "" >> .MIME_MAIL.tmp
  shift
done
echo "------$boundary--" >> .MIME_MAIL.tmp

## Send E-Mail
/usr/sbin/sendmail -i $to_addr < .MIME_MAIL.tmp

## Delete TEMP file
rm -f .MIME_MAIL.tmp

exit 0