MeCabの辞書に単語が重複した場合の挙動を調べてみた

以前、MeCabのユーザー辞書を作る方法を紹介しました。
参考: MeCabでユーザー辞書を作って単語を追加する

システム辞書に無い単語をユーザー辞書に登録して使えば、当然システム辞書の単語とユーザー辞書の単語の両方を使って形態素解析が行えるようになります。
この時にもし、システム辞書に登録済みの単語を改めてユーザー辞書に登録してしまったらどのような挙動になるのか気になったのでドキュメントを確認してみましたがそれらしい記載がありませんでした。(他サイトにユーザー辞書がシステム辞書を上書きするという情報もあったのですが、本当にそうなのか疑わしいとも思いました。)
そこで実験してみようと思ったのがこの記事です。

また、MeCabは起動時にシステム辞書は1つしか指定できませんが、ユーザー辞書は複数指定できます。その複数のユーザー辞書に登録したらどういう挙動になるのかも確認しました。
それとついでにですが、1個のユーザー辞書に同じ単語を複数回登録した場合(これはもうただの辞書作成時のミスでしかあり得ないのですが。)の事象も見ています。

え、システム辞書に登録されてる単語をユーザー辞書に登録することなんてある?と思われる方もいらっしゃると思いますが、これは普通にあります。気づかずに登録してしまった、という場合はもちろんですが、解析結果の誤りを修正するために生起コストの設定を変えたいというケースがあるのです。

例えば、IPA辞書そのままだと、「りんごジュース」の形態素解析結果は次のように誤ったものになります。

$ echo りんごジュース | mecab
りん	副詞,助詞類接続,*,*,*,*,りん,リン,リン
ご	接頭詞,名詞接続,*,*,*,*,ご,ゴ,ゴ
ジュース	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS

IPA辞書に「りんご」が登録されていないわけではありません。バッチリ含まれています。

# ビルド前のIPA辞書のファイルが含まれているディレクトリで実行
$ grep りんご * -r
Noun.csv:りんご,1285,1285,7277,名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ

「りんご」自体の生起コストが高いこととか、「BOS」と「名詞,一般」の連接コストなどの諸々の事情によりこのような誤りが発生しています。これを是正する手段の一つが、「りんご」をもっと低い生起コストで登録することなのです。

とりあえず、生起コストを5000に落としてやってみます。下のコードでcatしてるようなテキストをファイルを作り、ユーザー辞書をコンパイルしてMeCabを動かしてみます。

# seedファイルの中身確認
$ cat apple1.csv
りんご,1285,1285,5000,名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
# コンパイル
$ /usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic -u apple1.dic -f utf-8 -t utf-8 apple1.csv
reading apple1.csv ... 1
emitting double-array: 100% |###########################################|
done!
# 生成されたユーザー辞書を使って形態素解析(生起コストも表示)
$ echo りんごジュース | mecab -F %m\\t%c\\t%H\\n -u apple1.dic
りんご	5000	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS

ユーザー辞書に登録した生起コスト5000のりんごを使って形態素解析されましたね。
この結果だけ見ると、システム辞書にある単語をユーザー辞書に登録したら情報が上書きされたように見えます。ただし、実際の動きはそうでは無いのです。

上書きされたように見えるだけで、システム辞書とユーザー辞書それぞれのりんごは別々の独立した単語として処理されていて、解には生起コストが低いユーザー辞書のりんごが採用されたというのが正確な動きになります。このことはN-Best解を表示すると確認できます。

$ echo りんごジュース | mecab -F %m\\t%c\\t%H\\n -N3 -u apple1.dic
りんご	5000	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
りん	4705	副詞,助詞類接続,*,*,*,*,りん,リン,リン
ご	6655	接頭詞,名詞接続,*,*,*,*,ご,ゴ,ゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
りんご	7277	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS

3番目の解として、システム辞書の生起コスト7277のりんごもバッチリ登場していますね。上書きされて消えているわけでは無いのです。

つまりユーザー辞書に単語を登録しても、元のシステム辞書より高い生起コストを設定してたらそれは1番目の解としては使われないということです。apple2って名前で、生起コスト8000のりんごを登録してやってみます。

$ cat apple2.csv
りんご,1285,1285,8000,名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
$ /usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic -u apple2.dic -f utf-8 -t utf-8 apple2.csv
reading apple2.csv ... 1
emitting double-array: 100% |###########################################|
done!
$ echo りんごジュース | mecab -F %m\\t%c\\t%H\\n -u apple2.dic
りん	4705	副詞,助詞類接続,*,*,*,*,りん,リン,リン
ご	6655	接頭詞,名詞接続,*,*,*,*,ご,ゴ,ゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS

システム辞書だけの場合と結果変わりませんでしたね。このことからも、ユーザー辞書の単語がシステム辞書の単語を上書きする説は誤りであることがわかります。

実は元々、他のサイトの記事で単語が上書きされる説を見かけて、ユーザー辞書を複数登録したら最後にどっちの単語が残るんだ?という疑問からこの検証を始めています。
しかし、「そもそも上書きしないで別の単語として扱われる」が結論であれば、同じ辞書に複数回単語登録したり、ユーザー辞書を複数使用してそれぞれに重複してた単語があったとしても、別の単語として扱われて生起コストで判定される、と予想が付きます。

一応、「りんご」が2回登録された辞書も作って、上で作った2辞書と合わせて3辞書で動かしてみましょう。

$ cat apple3.csv
りんご,1285,1285,6000,名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
りんご,1285,1285,4000,名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
$ /usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic -u apple3.dic -f utf-8 -t utf-8 apple3.csv
reading apple3.csv ... 2
emitting double-array: 100% |###########################################|
done!
$ echo りんごジュース | mecab -F %m\\t%c\\t%H\\n -u apple1.dic,apple2.dic,apple3.dic -N6
りんご	4000	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
りんご	5000	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
りんご	6000	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
りん	4705	副詞,助詞類接続,*,*,*,*,りん,リン,リン
ご	6655	接頭詞,名詞接続,*,*,*,*,ご,ゴ,ゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
りんご	7277	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
りんご	8000	名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース	3637	名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS

3つの辞書に登録した4つのりんごと、システム辞書に元々あったりんごが全部使われていますね。

NBest解に登場する順番もシンプルに生起コストの順番になっています。
ユーザー辞書で指定した順番に上書きされて最後の辞書の一番最後の単語しか残らないんじゃ無いか、みたいなことを懸念していましたが、そんなことは全くありませんでした。

UAとGTMが導入済みのブログにGA4も設定してみた

2020年10月に正式にリリースされた GA4 (Google アナリティクス 4 プロパティ) をこのブログでも使うことにしました。このブログでは元々前世代のUA (ユニバーサルアナリティクス)を導入しています。現時点ではGA4よりUAの方が機能が充実しているように感じていますが、今後はGoogleさんがGA4の方に力を入れて改善していき、そちらをスタンダードにするということなので、使い始めた次第です。
ただ、いきなり乗り換えるのではなく当分並行稼働させていきます。

作業の前にこのブログでの設定状況についてです。このブログでは、Wordpressのプラグインを使って、GTM(Googleタグマネージャー)を導入し、タグマネージャーを経由してUAのタグを発火させていました。
参考: Google タグマネージャー導入
また、当然Googleアナリティクスのアカウント等も元々保有しています。無い場合はそこから作る必要があります。
あくまでもこの記事は、すでにUA+GTMが稼働中のページにGA4を追加する手順です。

では進めていきましょう。

手順1. GA4のプロパティを作成する。
以下の手順で作成できます。

GA4ではUAとは別のプロパティを作成し使用する必要があります。
1. Googleアナリテクスにアクセスする。
2. 左ペイン一番下の「管理」をクリックする。
3. プロパティ のところにある、 + プロパティを作成 をクリックする。
4. プロパティの設定をする。
– プロパティ名に自分がわかりやすい名前を入力する。(僕は「分析ノートGA4」にしました。)
– レポートのタイムゾーンは日本を選択
– 通貨に日本円を選択
5. 次へをクリック
6. ビジネスの概要設定画面が出てくるのでサイトの特性に合わせて適切なものを選びます。
– 悩んだのですが、業種はコンピュータ、電気製品にしました。
– 一人で更新しているので、ビジネスの規模は小規模-従業員数1〜10名にしています。
7. 利用目的を聞かれるので、該当するものを選ぶ。
– 自分は次の二つを選びました。
サイトまたはアプリでの顧客エンゲージメントを測定する
サイトまたはアプリの利便性を最適化する
8. 作成をクリックする

少しステップが多いですが、画面に従い順次行えば途中で迷うことはないと思います。

手順2. データストリームの設定
プロパティができたら続いてデータストリームを設定します。このブログはWeb版しか無い(アプリなど提供していない)のでWebのデータストリームを作成します。
上記のプロパティの作成から続けて行えますが、一度閉じてしまった場合は設定から開きましょう。
1. ウェブを選択する。
2. ウェブサイトのURLとストリーム名を入力します。URLはhttps://analytics-note.xyz ですが、 ストリーム名はどうするか悩みました。複数のストリームを同時に使う予定はなかったので、analytics-note としています。Webとアプリを両方分析する人はそれぞれ見分けられる名前が良いと思います。
3. ストリームを作成をクリックする。
4. 観測用のIDが生成されるのでメモしておきます。GTMで使います。
観測用のIDは G-{アルファベットと数字}の形式になっています。

以上で、GA側の設定は終わりです。あとはなんらかの方法で発行されたIDや、観測用のタグをブログの方に埋め込む必要があります。今回は導入済みのGTMを使いました。

手順3. GTMにGA4計測タグを追加

すでにGTMに作成済みのコンテナをそのまま使います。
1. GTM にアクセスする。
2. 既存のコンテナを選択する。
3. 左ペインでタグを選択し、新規をクリックする。
4. [タグの設定] をクリックして [GA4 設定] を選択する。
5. 先ほどの測定 ID「G-XXXXXXXXXX」を入力する。
6. トリガーをクリックする。
7. All Pagesを選択し、保存をクリックする。
8. デフォルトで、 Google アナリティクス GA4 設定 という名前が入ってたのでそのまま保存する。

これでタグが作成されたので、これを公開するための手順を続けていきます。
9. ワークスペースに戻ってプレピューをクリック。
10. Connect Tag Assistant to your site とメッセージが表示されたら、
https://analytics-note.xyz/ と対象サイトのURLを入力してConnectをクリックする。
11. そのブラウザでいくつかのページにアクセスすると、別のデバック用に開いていたブラウザのタブで開いていたページで発火したタグをみることができる。(昔のGTMは画面下部で確認していたので、この仕様が変わっていたようです。)
12. Google アナリティクス GA4 設定がFired(発火)になっているのを確認する。
13. ついでにWordpressの管理画面にもアクセスしてそこは発火しないことも確認する。
14. Tag Assistant の小さいウィンドウの Finish を押してプレビューを終了する。
16. 「公開」ボタンをクリックする。
17. バージョン名と説明を求められるので入力し、再度「公開」をクリックする。

以上で、GA4のが設定が完了し、データ収集が始まります。動作テストとして、リアルタイムビューを見てみるのがおすすめです。

追加で、最低限の設定として以下の設定を入れました。

データ保持期限を14ヶ月に伸ばす(デフォルトは2ヶ月)
こちらは、設定の、プロパティの データ設定 > データ保持 から設定できます。デフォルトの保持期間はかなり短いので伸ばしておいた方が良いでしょう。

また、Googleシグナルを有効にしました。
こちらも データ設定 > データ収集画面 から設定できます。

UAとGA4を並行してみていると、ユーザー数の集計値に差分が生まれていたり、なくなってしまった指標があったり、UAの方が用意されているレポートが多くて便利に感じたりと色々差があり、現時点ではまだUAの方が良いツールに感じることが多々あります。

ただ、Googleさんの方針として今後の開発はGA4の方に注力していくとのことですので、将来的に便利なツールになっていくことを期待しながら少しずつGA4に慣れていきたいと思います。

2022年のご挨拶と今年の方針

新年明けましておめでとうございます。本年もよろしくお願いします。

さて、今年のこのブログの更新方針について決めたのでまとめておきます。
昨年末の記事でも少し頭出ししていましたが、ブログに限らず今年の計画や目標について考え、今年1年はアウトプットよりもインプットを重視した年にしようと決めました。また、その内容もデータサイエンス関連に限らず幅広く吸収していく年にしたいです。

アウトプットの時間は減らしたいのとインプット内容にこのブログ記事につながるようなテーマの物が減るということで、このブログの更新ペースは落とします。昨年の半分くらいにして週1回更新、年間50記事程度を目標にゆっくりやっていこうと思います。もし書きたいことがありすぎて困るようなことになったらまたその時にペースを見直すかもしれませんが。

僕はもともと読書が好きで色々なジャンルの本を幅広く読んでいました。その後、2017年に転職してデータサイエンティストになってからこの5年ほどの期間、まずは仕事で使うデータ分析のスキルを優先しようということで読む本がほとんど広い意味でのデータサイエンス関連や、ドメイン知識としての人材業界関連の本ばかりになっていました。特にそれが不満というわけでもなく、どんどん新しい知識が身に付き、できることが増えていくことにやりがいも感じていました。この分野は本当に学ぶことが多く、この先も興味が尽きることはなさそうです。ただその一方で、趣味に関する本とか書店でたまたま見かけて興味を持った本とか話題のベストセラー等々の他の読みたい本を読むのが完全に後回しになってきたのも事実です。

今年もデータ分析の勉強は継続はしますし、今の時点で絶対読みたいと思ってる本はそこそこあるのですがが、それらを読むのは月に1〜2冊程度に抑えようと思ってます。そして浮いた時間はまた昔みたいに、仕事や実用性を無視して興味を持ったものを何でも読んでいく時間にします。

その他、流石に3年も運用しているとこのブログにも色々改善したい点あったり、内容が古くなってしまった記事などもあります。新規の記事を書く時間を減らした範囲内で、過去記事の見直しなどを含めたメンテナンスにも細々と着手しようと思います。例えば「プログラミング」っていう非常に雑なカテゴリに多くの記事が集中してしまっているのでこの辺の見直しもしたいです。

以上のような方針のためこのブログの更新は昨年に比べてゆっくりになりますが、本年もよろしくお願いいたします。

2021年のまとめ

2021年の最後の投稿になります。

本年も訪問者の皆様には大変お世話になりました。書いた記事が多くの方に読んでいただけたということはもちろんですが、土日祝日なども平日より少ないとはいえ多くのアクセスがあり、休日も技術的な調べ物をしている熱心な人たちがいると実感できることは自分が学習を続けていく上でも大きな励みになりました。

今年も1年間の振り返りをやっていきたいと思います。本年までの累積の記事数および、年間のアクセス数は次のようになりました。
– 累計記事数 514記事 (この記事含む。昨年時点 409記事)
– 訪問ユーザー数 200,661人 (昨年実績 146,674人)
– ページビュー 348,595回 (昨年実績 258,698回)

年間100記事更新の目標を無事に達成でき、それに伴って訪問者の数も増えているので達成感を感じています。

とはいえ、多くのpvを集めているのは古い記事が多く、今年特に力を入れて書いたMeCabのアルゴリズムの話や、AWSのAI関連サービスの話、トレジャーデータの小ネタなどはあまり読まれていないようです。テーマ選びなのか僕の文章力なのか、なんらかの課題はあるように感じています。

さて、恒例のよく読まれた記事ランキングを見ていきましょう。
今回は2021年1年間でのPV数によるランキングです。

  1. PREANFA (プリアンファ) PREGEL Muse カラージェル クリアブラウン211 3g ジェルネイル (昨年1位)
  2. ネットワークグラフの中心性 (New)
  3. Pythonで連続した日付のリストを作る (New)
  4. pyenvで作成した環境を消す (New)
  5. TensorflowやKerasでJupyterカーネルが落ちるようになってしまった場合の対応 (New)
  6. numpyのpercentile関数の仕様を確認する (昨年4位)
  7. INSERT文でWITH句を使う (昨年7位)
  8. matplotlibでグラフ枠から見た指定の位置にテキストを挿入する (New)
  9. MIKIROKU(限定盤)(DVD付) (昨年3位)
  10. Pythonで多変量正規分布に従う乱数を生成する (昨年10位)

Googleアナリティクスで確認した時、1位と10位が昨年と同じなので今年もあまり変わり映えしないなという印象を持っていました。しかし、改めて昨年のランキングと比較してみると昨年ランクインしなかった記事が5記事も入っており意外と顔ぶれ変わってましたね。

このブログもこれで開設から丸三年になります。流石にネタ切れを感じる日もあるので来年の運用をどうしようかと考えています。(とはいえ、ブログネタのストックは今時点で40個程度はあるので本当の意味ではネタ切れしてないのですが、書きたいけどなかなか筆が進まないものやタイミングを逃した感があるのも多く難しいところです。)

来年も技術的なスキルアップを目指した学習はもちろん続けていきますし、仕事の中での疑問や課題感からネタが出てくることもあると思うので、ブログの更新自体は続けていきます。ただ、技術関連以外のインプットにももっと力を入れていきたいですし、休日の時間を今以上に読書や講座受講などに使いたいので、更新頻度は見直したほうがいいかもとは思っています。

この年末年始で来年をどう過ごすかを考えて、その中でブログの運用方針も決めたいと思います。

それではみなさん、今年も1年間ありがとうございました。良いお年を。

pipでライブラリをインストールする前に依存ライブラリを確認する

僕はAnacondaで環境を構築してcondaで運用しているのですが、どうしてもcondaでは入れられないライブラリがある時など、やむを得ずpipを使うことがあります。その場合、condaで入れられる限りの依存ライブラリを入れた後に必要最小限のライブラリをpipで入れるようにしているのですが、依存ライブラリの確認漏れ等があり、想定外のライブラリがpipで入ってしまうことがありました。(この運用もそろそろ限界を感じていて、次に環境を作り直す機会があったらpipで統一したいと思っています。)

問題の一つはpipでインストールする前に依存ライブラリを調べる方法が分かりにくかったことだと思っていたのですが、ようやく事前に調べかたがわかったのでそれを紹介します。

どうやら、PyPI の特定のURLでアクセスできるJSONファイルに、必要な情報が載っているようです。ここに書いてありました。
参考: PyPIJSON – Python Wiki

バージョンを指定しない場合は、
https://pypi.python.org/pypi/<package_name>/json
バージョンを指定する場合は、
https://pypi.python.org/pypi/<package_name>/<version>/json
というURLにアクセスすると、そのパッケージ(ライブラリ)の情報が取得できます。

試しに jupyter notebook (pip install notebook でインストールするので、ライブラリ名はnotebook)の情報ページである
https://pypi.python.org/pypi/notebook/json
にアクセスしていただくと分かりますが、かなりでかいJSONが得られます。

ここからテキストエディターで必要な情報を得るのは骨が折れるので、Python使って欲しい情報を探しましょう。

偶然見つけたのですが、 pprint というメソッドのドキュメントでの使用例がこのJSONの表示だったりします。そこでは urllibを使っていますがこれは若干使いにくいので僕はrequestsを使います。
参考: requestsを使って、Webサイトのソースコードを取得する

では、試しに notebook の 情報をとってみましょう。

import requests
package_name = "notebook"
url = f"https://pypi.org/pypi/{package_name}/json"
json = requests.get(url).json()
# このJSONはかなりでかい
print(len(str(json)))
# 113699
# JSONのkeys。 この中の info が必要な情報を含んでいる。
print(json.keys())
# dict_keys(['info', 'last_serial', 'releases', 'urls', 'vulnerabilities'])
# infoの下に、多くの情報がある。
print(json["info"].keys())
"""
dict_keys(['author', 'author_email', 'bugtrack_url', 'classifiers',
        'description', 'description_content_type', 'docs_url', 'download_url',
        'downloads', 'home_page', 'keywords', 'license', 'maintainer',
        'maintainer_email', 'name', 'package_url', 'platform', 'project_url',
        'project_urls', 'release_url', 'requires_dist', 'requires_python',
        'summary', 'version', 'yanked', 'yanked_reason'])
"""
# requires_dist が依存ライブラリの情報。リスト形式なので、順番に表示する
for requires_dist_text in json["info"]["requires_dist"]:
    print(requires_dist_text)
"""
jinja2
tornado (>=6.1)
pyzmq (>=17)
argon2-cffi
ipython-genutils
traitlets (>=4.2.1)
jupyter-core (>=4.6.1)
jupyter-client (>=5.3.4)
nbformat
nbconvert
nest-asyncio (>=1.5)
ipykernel
Send2Trash (>=1.8.0)
terminado (>=0.8.3)
prometheus-client
sphinx ; extra == 'docs'
nbsphinx ; extra == 'docs'
sphinxcontrib-github-alt ; extra == 'docs'
sphinx-rtd-theme ; extra == 'docs'
myst-parser ; extra == 'docs'
json-logging ; extra == 'json-logging'
pytest ; extra == 'test'
coverage ; extra == 'test'
requests ; extra == 'test'
nbval ; extra == 'test'
selenium ; extra == 'test'
pytest-cov ; extra == 'test'
requests-unixsocket ; (sys_platform != "win32") and extra == 'test'
"""
# requires_python で Pythonのバージョンの指定も見れる
print(json["info"]["requires_python"])
# >=3.6

extra がついているのはオプション付きでインストールする時に必要になる物なので、基本的に、次のライブラリが必要であることがわかりますね。
jinja2
tornado (>=6.1)
pyzmq (>=17)
argon2-cffi
ipython-genutils
traitlets (>=4.2.1)
jupyter-core (>=4.6.1)
jupyter-client (>=5.3.4)
nbformat
nbconvert
nest-asyncio (>=1.5)
ipykernel
Send2Trash (>=1.8.0)
terminado (>=0.8.3)
prometheus-client

ちょっとテストしてみましょう。 pyenv で新しい環境作って、notebook入れてみます。
(version 3.8.7と微妙に古いバージョン入れていますがこれは適当です。

# 新しい仮想環境を構築
$ pyenv install 3.8.7
# 環境切り替え
$ pyenv global 3.8.7
# ライブラリが何も入ってないことを確認(出力がない)
$ pip freeze
# notebook インストール
$ pip install notebook
# 依存ライブラリと共にインストールされたことを確認
$ pip freeze
appnope==0.1.2
argon2-cffi==21.3.0
argon2-cffi-bindings==21.2.0
attrs==21.2.0
backcall==0.2.0
bleach==4.1.0
cffi==1.15.0
debugpy==1.5.1
decorator==5.1.0
defusedxml==0.7.1
entrypoints==0.3
importlib-resources==5.4.0
ipykernel==6.6.0
ipython==7.30.1
ipython-genutils==0.2.0
jedi==0.18.1
Jinja2==3.0.3
jsonschema==4.3.2
jupyter-client==7.1.0
jupyter-core==4.9.1
jupyterlab-pygments==0.1.2
MarkupSafe==2.0.1
matplotlib-inline==0.1.3
mistune==0.8.4
nbclient==0.5.9
nbconvert==6.3.0
nbformat==5.1.3
nest-asyncio==1.5.4
notebook==6.4.6
packaging==21.3
pandocfilters==1.5.0
parso==0.8.3
pexpect==4.8.0
pickleshare==0.7.5
prometheus-client==0.12.0
prompt-toolkit==3.0.24
ptyprocess==0.7.0
pycparser==2.21
Pygments==2.10.0
pyparsing==3.0.6
pyrsistent==0.18.0
python-dateutil==2.8.2
pyzmq==22.3.0
Send2Trash==1.8.0
six==1.16.0
terminado==0.12.1
testpath==0.5.0
tornado==6.1
traitlets==5.1.1
wcwidth==0.2.5
webencodings==0.5.1
zipp==3.6.0

予想してたよりずっと多くのライブラリがインストールされましたね。どうやら依存ライブラリたちの依存ライブラリ、もちろんそれらの依存ライブラリも順次インストールされたようです。ただ、一つずつ確認したところ、JSONから取得した依存ライブラリは全て入ったことがわかります。

これは実験しておいてよかったです。必ずしも、JSONから得られたライブラリだけが入るわけではないことがわかりました。

もう一点補足しておくと、requires_dist には必ず値が入っているわけではありません。当然ですが依存ライブラリがないライブラリもあります。その場合は空配列になっているのかな、と思ったのですが、 null になるようですね。 NumPyなどがそうです。

package_name = "numpy"
url = f"https://pypi.org/pypi/{package_name}/json"
json = requests.get(url).json()
print(json["info"]["requires_dist"])
# None

以上で、pipインストール前にライブラリの依存ライブラリを調べられるようになりました。

ここで取得したJSONは他にも様々な情報を持っているようなので、それらも調べておこうと思います。

pandas.DataFrameのgroupby関数で計算した結果を各行に展開する

なんとなくドキュメントを眺めていたら、groupby().transform()っていう便利そうな関数を見つけたのでその紹介です。

DataFrameのgroupbyといえば、指定した列をキーとしてグループごとの合計や平均、分散、個数などの集計を行うことができる関数です。

通常は、集計したキーの数=グループの数の行数のDataFrameを戻り値として返してきます。

import pandas as pd
df = pd.DataFrame(
    {
        "category": ["A", "A", "A", "B", "B"],
        "amount": [100, 300, 100, 200, 200],
    }
)
print(df)
"""
  category amount
0        A    100
1        A    300
2        A    100
3        B    200
4        B    200
"""
print(df.groupby("category").sum())
"""
category
A            500
B            400
"""

ここで、この groupby して得られた集計値を、元のDataFrameの各業に展開したいことがあります。
そのような場合、僕はpd.mergeでデータフレームを結合するか、辞書形式に変換して結合することが多かったです。
例えば以下のようなコードになります。

# mergeで結合する場合
group_df = df.groupby("category").sum()
group_df.reset_index(inplace=True)
group_df.rename(columns={"amount": "category_amount"}, inplace=True)
print(pd.merge(df, group_df, on="category", how="left"))
"""
  category  amount  category_amount
0        A     100              500
1        A     300              500
2        A     100              500
3        B     200              400
4        B     200              400
"""
# 辞書を作ってマッピングする場合
group_df = df.groupby("category").sum()
sum_dict = group_df.to_dict()["amount"]
print(sum_dict)
# {'A': 500, 'B': 400}
df["category_amount"] = df["category"].apply(sum_dict.get)
print(df)
"""
  category  amount  category_amount
0        A     100              500
1        A     300              500
2        A     100              500
3        B     200              400
4        B     200              400
"""

書いてみるとこれらの手順を踏んでもそんなに複雑ではないのですが、やっぱり一発でできるともっと便利です。

そこで使えるのが、冒頭で紹介した、transformです。
参考: pandas.core.groupby.DataFrameGroupBy.transform

これは元のデータフレームと同じインデックスを持つデータフレームとして、GroupByの結果を返してくれます。ちょっとやってみます。

df = pd.DataFrame(
    {
        "category": ["A", "A", "B", "B", "B"],
        "amount": [100, 300, 100, 200, 200],
    }
)
# 元のDataFrameと同じ行数で、対応する行の"category"列の値が含まれるグループの合計を返す
print(df.groupby("category").transform("sum"))
"""
   amount
0     400
1     400
2     500
3     500
4     500
"""
# 元のDataFrameに合計値を付与したい場合は次のようにできる
df["category_amount"] = df.groupby("category").transform("sum")["amount"]
print(df)
"""
  category  amount  category_amount
0        A     100              400
1        A     300              400
2        B     100              500
3        B     200              500
4        B     200              500
"""

1行で済みましたね。

この新しく作った列を使えば、一定件数以下しか存在しないカテゴリの行を削除するとか、カテゴリごとにそれぞれの要素のカテゴリ内で占めてる割合を計算するとか、それぞれの要素のカテゴリごとの平均との差異を求めるとかそういった計算が非常に容易にできるようになります。

そしてさらに、このtransform とlambda関数を組み合わせて使うと、カテゴリの平均との差を一発で出す、といったこともできます。

df = pd.DataFrame(
    {
        "category": ["A", "A", "B", "B", "B"],
        "amount": [100, 300, 100, 200, 200],
    }
)
print(df.groupby("category").transform(lambda x: x-x.mean()))
"""
       amount
0 -100.000000
1  100.000000
2  -66.666667
3   33.333333
4   33.333333
"""

lambda 関数に渡されている x はそれぞれの行の値のように振る舞ってくれるにもかかわらず、同時に x.mean() でグループごとの平均を出すこともでき、その差分を元のDataFrameとインデックスを揃えて返してくれています。

これは使いこなせば相当便利なメソッドになりそうです。

MeCabで分かち書き済みの単語に対して品詞を判定する

MeCabで形態素解析してテキストを単語に分解するとき、分かち書きしたテキストと、品詞情報が得られます。その単語の出現頻度等を集計した後で、この単語はこの品詞、という情報を付与して絞り込み等をやりたくなったのでその方法をメモしておきます。

実は以前ワードクラウドを作った時に品詞別に色を塗るために似たようなコードを作っています。今回の記事はその改良版です。
参考: WordCloudの文字の色を明示的に指定する

この記事では次のようなコードを使いました。(参照した記事は先行するコードでMeCabのTaggerインスタンスを作ってる前提なのでその辺ちょっと補って書きます。)

import MeCab
tagger = MeCab.Tagger()
def get_pos(word):
    parsed_lines = tagger.parse(word).split("\n")[:-2]
    features = [l.split('\t')[1] for l in parsed_lines]
    pos = [f.split(',')[0] for f in features]
    pos1 = [f.split(',')[1] for f in features]
    # 名詞の場合は、 品詞細分類1まで返す
    if pos[0] == "名詞":
        return f"{pos[0]}-{pos1[0]}"
    # 名詞以外の場合は 品詞のみ返す
    else:
        return pos[0]

参照した記事で補足説明書いてますとおり、このコードは単語をもう一回MeCabにかけて品詞を取得しています。その時に万が一単語がさらに複数の形態素に分割されてしまった場合、1つ目の形態素の品詞を返すようになっています。

このコードを書いた時、単語がさらに分解されるってことは理論上はありうるけど、滅多にないだろう、と楽観的に考えていました。ところが、色々検証していると実はそんな例が山ほどあることがわかってきました。

例えば、「中国語」という単語がありますが、これ単体でMeCabに食わせると「中国」と「語」に分かれます。以下が実行例です。

# 形態素解析結果に「中国語」が出る例
$ echo "彼は中国語を話す" | mecab
彼	名詞,代名詞,一般,*,*,*,彼,カレ,カレ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
中国語	名詞,一般,*,*,*,*,中国語,チュウゴクゴ,チューゴクゴ
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
話す	動詞,自立,*,*,五段・サ行,基本形,話す,ハナス,ハナス
EOS
# 「中国語」がさらに「中国」 と「語」に分かれる
$ echo "中国語" | mecab
中国	名詞,固有名詞,地域,国,*,*,中国,チュウゴク,チューゴク
語	名詞,接尾,一般,*,*,*,語,ゴ,ゴ
EOS

「中国語」が固有名詞、地域、国と判定されるとちょっと厄介ですね。

他にも、「サバサバ」は「サバ」「サバ」に割れます。

$ echo "ワタシってサバサバしてるから" | mecab
ワタシ	名詞,固有名詞,組織,*,*,*,*
って	助詞,格助詞,連語,*,*,*,って,ッテ,ッテ
サバサバ	名詞,サ変接続,*,*,*,*,サバサバ,サバサバ,サバサバ
し	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
てる	動詞,非自立,*,*,一段,基本形,てる,テル,テル
から	助詞,接続助詞,*,*,*,*,から,カラ,カラ
EOS
$ echo "サバサバ" | mecab
サバ	名詞,一般,*,*,*,*,サバ,サバ,サバ
サバ	名詞,一般,*,*,*,*,サバ,サバ,サバ
EOS

他にも「ありえる」が「あり」「える」とか、「無責任」が「無」「責任」とか「ビュッフェ」が「ビュッ」「フェ」など、かなりの種類の単語が再度分解されます。

ということで、冒頭にあげた get_pos メソッドは思っていたよりもずっと誤判定しやすいということがわかってきました。

前置きが長くなってきましたが、このことを踏まえて、単語を再度分割することのないようにその単語としての品詞情報を取得できないかを考えました。

結局、制約付き解析機能を使って実現できそうだということがわかりました。
参考: MeCabの制約付き解析機能を試す

要するに、MeCabに渡された単語はそれで1単語だ、という制約を課せば良いわけです。

そのためには、-pオプション付きでTaggerを生成し、「{単語}{タブ}*(アスタリスク)」という形式のテキストに変換してTaggerでparseすれば大丈夫です。

Pythonのコードで書くと次のようになりますね。

import MeCab
tagger = MeCab.Tagger("-p")
def get_pos(word):
    # 制約付き解析の形態素断片形式にする
    p_token = f"{word}\t*"
    # 出力のEOS部分を捨てる
    parsed_line = tagger.parse(p_token).splitlines()[0]
    feature = parsed_line.split("\t")[1]
    # ,(カンマ)で区切り、品詞,品詞細分類1,品詞細分類2,品詞細分類3 の4項目残す
    pos_list = feature.split(",")[:4]
    # もう一度 ,(カンマ) で結合して返す
    return ",".join(pos_list)
# 利用例
print(get_pos("中国語"))
# 名詞,一般,*,*

上のコードは、品詞を再分類3まで取得するようにしましたが、最初の品詞だけ取得するとか、*(アスタリスク)の部分は省略するといった改修はお好みに合わせて容易にできると思います。

これで一旦今回の記事の目的は果たされました。

ただ、元の文中でその単語が登場したときの品詞が取得されているか、という観点で見るとこのコードも完璧ではありません。

表層系や原型が等しいが品詞が異なる単語が複数存在する場合、通常のMeCabの最小コスト法に則って品詞の一つが選ばれることになります。BOS/EOSへの連接コストとその品詞の単語の生起コストが考慮されて最小になるものが選ばれる感じですね。

分かち書き前のテキストで使われていたときの品詞が欲しいんだ、となると後からそれを付与するのは困難というより不可能なので、分かち書きした時点でちゃんと保存してどこかに取っておくようにしましょう。

あとおまけで、このコードを書いてる時に気づいたMeCabの制約付き解析機能の注意点を書いておきます。MeCabを制約付き解析モードで使っている時に、「表層\t素性パターン」”ではない”テキスト、つまり文断片と呼ばれている文字列を改行コード付けずに渡すとクラッシュするようです。
-p 付きで起動したときは、「表層\t素性パターン」形式の形態素断片か改行コードを必ず含むテキストで使うようにしましょう。

jupyter notebookでやると カーネルごとお亡くなりになりますので特に要注意です。

ちょっとコンソールでやってみますね。

$ python
>>> import MeCab
>>> tagger = MeCab.Tagger("-p")
>>> tagger.parse("中国語")
Segmentation fault: 11
# これでPythonが強制終了になる
$

改行コードつければ大丈夫であることは以下のようにして確認できます。

$ python
>>> import MeCab
>>> tagger = MeCab.Tagger("-p")
>>> tagger.parse("中国語\n")
'中国\t名詞,固有名詞,地域,国,*,*,中国,チュウゴク,チューゴク\n語\t名詞,接尾,一般,*,*,*,語,ゴ,ゴ\nEOS\n'

-p をつけてないときは別に改行コードなしのテキストも読み込んでくれるのでこれはちょっと意外でした。

制約付き解析(-p付き)でMeCabを使っている時に、「Segmentation fault: 11」が出たらこのことを思い出してください。

jupyter notebookのセルの出力をコードでクリアする

諸事情ありまして、jupyter notebookのセルの出力をクリアする方法を知りたくなったので調べました。
通常、jupyterではテキストを複数回にわたってprintしたり、matplotlibの図をいくつも出力するコードを1つのセルに書くと、出力したテキストなり図なりがダーっと続けて出てきます。
ちょっとこれを逐一クリアして新しいものだけ残すようにしたかったのです。
(こんなことする必要があることは滅多にないのですが。)

実は、クリアしたい対象がprintした1行以内のテキストの場合、それを実装する方法は過去に紹介したことがあります。それはprintメソッドのend引数を使ってprint後に改行コードを出力しないようにし、キャリッジリターン(“\r”)で出力位置を行頭に戻して空白で上書きしてしまうというものです。
これ使ってプログレスバーを作った記事が過去にありますね。
参考: printでお手軽プログレスバー

例えば、jupyterで次のコードを動かすと0~49まで数字がカウントアップします。
\r でカーソルを先頭にもどして、空白で埋めて、最後に次のprintのためにもう一回カーソルを先頭に戻しています。 end=”” はprint後に改行させない設定です。
sleep() は入れておかないと一瞬すぎて何も見えないのでウェイトとして入れています。

import time
for i in range(50):
    print("\r          \r", end="")
    print(i, end="")
    time.sleep(0.5)

ただ、さっきも書きましたがこの方法だと1行のテキストしか消せません。

複数行の出力だったらどうやって消すのかなと思って調べた結果見つかったのが、IPython モジュールにあった、 clear_output というメソッドです。
正確には、IPython.display.clear_output として実装されています。
ドキュメントはこちらです。
参考: Module: display — IPython 7.30.1 documentation

Clear the output of the current cell receiving output. とある通り、これが実行されるとそのステップが含まれたセルの出力だけを消してくれます。他のセルの出力は残してくれるので安心ですね。

wait (デフォルトはFalse)という便利な引数も持っています。これは、Falseにしておくと即座に出力を消すのに対して、Trueを渡すと、次の出力がくるのを待って消してくれます。連続して何かを出力するようなコードの場合、Trueにしておくと出力をスムーズに入れ替えるような動きになるのです。 Falseだと一瞬何も出力がない状態になるので次のセルとの間が詰まって 以降のセルがガクガク動きます。

以下のようにして、1秒ごとに現在時刻を表示する時計のような出力も出せます。

from IPython.display import clear_output
from datetime import datetime
import time
for i in range(10):
    print("現在時刻\n", datetime.now())
    clear_output(True)
    time.sleep(1)
"""
現在時刻
 2021-12-14 23:58:34.942141
上のような出力が1秒ごとに更新されて書き換えられる
"""

clear_outputはテキストだけではなく、図もクリアしてくれます。これを応用すると、パラパラ漫画のようにして手軽にアニメーションを作ることができます。

徐々にデータが増えて延びる折れ線グラフを描いてみたのが次のコードです。

import matplotlib.pyplot as plt
import numpy as np
# プロットする点を格納する配列
X = []
Y = []
for i in range(100):
    # 新しい点を追加する
    X.append(i)
    Y.append(np.random.randn())  # y座標には乱数入れる
    clear_output(True)  # それまでの出力をクリアする
    # グラフ作図
    fig = plt.figure(facecolor="w")  # 出力をクリアしたら改めてfigreオブジェクトが必要らしい
    ax = fig.add_subplot(111)
    ax.plot(X, Y)
    # グラフ表示
    plt.show()
    time.sleep(0.1)

このコードで jupyter 上にはアニメーションが表示できます。

実質的には clear_output(True) を差し込んでるだけなので、かなり手軽ですね。
ただ、これには一つ欠点もあって、jpyter上で簡易的に図を書いたり消したりしてアニメーションっぽく見せているだけなのでこのまま動画として保存することはできません。
(そのためこの記事にも結果の画像を貼っていません)

もし、gif形式などで保存したい場合は、少々面倒になるのですが、 ArtistAnimation などを使いましょう。過去の記事で取り上げています。
参考: matplotlibでgif動画生成

subprocessでパイプラインの実装

前回に続いてsubprocessの話です。予告していた通り、PythonでOSコマンドをパイプラインで繋いで実行する方法を紹介します。

まず前提ですが、subprocess.run にパイプラインを含むOSコマンドを渡してもそのままでは動きません。例えば実行中のプロセスから jupyter の文字を含む次のようなコマンドを考えます。

$ ps aux | grep jupyter
yutaro             762   0.0  0.8  4315736  67452 s000  S    11:55PM   0:03.71 {Pythonのパス} {pyenvのパス}/versions/anaconda3-2019.10/bin/jupyter-notebook
yutaro             910   0.0  0.0  4278648    712 s000  S+   12:04AM   0:00.00 grep jupyter

このコマンドをそのまま subprocess に渡しても動かないわけです。

import subprocess
cp = subprocess.run(
    ["ps", "aux", "|", "grep", "jupyter"],
    capture_output=True,
    text=True
)
# リターンコードが0ではない
print(cp.returncode)
# 1
# 標準出力は空っぽ
print(cp.stdout)
#
# 標準エラー出力にはエラーが出ている
print(cp.stderr)
"""
ps: illegal argument: |
usage: ps [-AaCcEefhjlMmrSTvwXx] [-O fmt | -o fmt] [-G gid[,gid...]]
          [-u]
          [-p pid[,pid...]] [-t tty[,tty...]] [-U user[,user...]]
       ps [-L]
"""

実は、パイプラインを含むコマンドを簡単に動かす方法はあります。それがshell引数にTrueを渡すことです。これは渡されたコマンドをシェルによって実行するオプションです。この場合、コマンドは空白で区切った配列ではなく一つの文字列で渡します。

cp = subprocess.run(
    "ps aux | grep jupyter",
    capture_output=True,
    text=True,
    shell=True
)
# リターンコードは0
print(cp.returncode)
# 0
# 標準出力に結果が入る
print(cp.stdout)
# 結果略。
# 標準エラー出力は空
print(cp.stderr)
# 

ただし、ドキュメントに「注釈 shell=True を使う前に セキュリティで考慮すべき点 を読んでください。」という注釈がついてるように、これはセキュリティ面で問題がある方法のようです。
参考: セキュリティで考慮すべき点
シェルインジェクションを避けるのはアプリ側の責任だって書いてありますね。この点気をつけて使いましょう。

さて、色々検証してみたのですが、 shell=True を使わなくてもパイプラインを実装する方法はあるようです。それは単純に標準入力を使う方法で、1個目のコマンドの標準出力を2個目のコマンドの標準入力に渡してあげます。

とりあえず、パイプラインではなく単一のコマンドで標準入力を使ってみましょう。macabコマンドに、いつもの「すもももももももものうち」を渡してみます。

runメソッドに標準入力を渡すには、 input という引数を使います。これで注意しないといけないのは、inputには”バイト列”でデータを渡す必要があることです。str型だとエラーになるので、encode() してから渡します。ただ、text=True も指定するときは逆にstrで渡さないといけないようですね。

text = "すもももももももものうち"  # 入力するテキスト
text_byte = text.encode()  # byte型にエンコード
cp = subprocess.run(
    "mecab",
    capture_output=True,
    input=text_byte  # 通常はbyte型で標準入力を渡す
)
# byte型でデータが返ってきているので、decode()して表示
print(cp.stdout.decode())
"""
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
"""
# text=True を指定するときは str型で標準入力を渡す
cp = subprocess.run(
    "mecab",
    capture_output=True,
    text=True,
    input=text  # text=True を指定するときは str型で標準入力を渡す
)
# str型で格納されているのでそのままprintできる
print(cp.stdout)
"""
結果は同じなので略
"""

さて、標準入力の渡し方がわかったら、あとは先行するコマンドの標準出力を次のコマンドの標準入力に渡すだけです。

最初の ps aux | grep jupyter でやってみましょう。

cp1 = subprocess.run(
    ["ps", "aux"],
    capture_output=True,
    text=True,
)
cp2 = subprocess.run(
    ["grep", "jupyter"],
    capture_output=True,
    text=True,
    input=cp1.stdout  # 一つ目のコマンドの標準出力を渡す
)
print(cp2.stdout)
"""
yutaro             762   0.0  0.8  4315736  67720 s000  S    11:55PM   0:05.04 {Pythonのパス} {pyenvのパス} /versions/anaconda3-2019.10/bin/jupyter-notebook
"""

この記事の先頭のコマンドの結果と微妙に異なりますね。 grep jupyter のプロセスが出てきません。これは、ps aux だけ先行して動かし、その結果をもとにgrepしているので、厳密にはシェルでパイプラインしたのとは異なるからそうなっているのでしょう。

ただ、通常の用途であればほぼ同じ結果が得られると思います。
どうしても差分が気になるのであれば shell=Trueの方の方法を使うことも検討が必要でしょうね。

サンプルとして選んだコマンドがイマイチだったので、厳密にいうと再現できてないサンプルを提示してしまったのですが、このようにして、PythonでOSコマンドのパイプラインが再現できます。

WORLD OF FINAL FANTASY Original Soundtrack

このブログの過去記事でもすでに使ったことがあるのですが、改めてsubprocessの使い方をまとめておきます。
ドキュメントはこちら。
参考: subprocess — サブプロセス管理 — Python 3.10.0b2 ドキュメント

subprocessは os.system を置き換えるために作られた新し目のモジュールらしいので、僕も新しい方法としてこれを使っていたのですが、Python 3.5 から subprocess に run() というメソッドが実装され、僕が書いていた方法はいつの間にか古い方法になってしまっていたようです。ドキュメントを少し引用します。

サブプロセスを起動するために推奨される方法は、すべての用法を扱える run() 関数を使用することです。より高度な用法では下層の Popen インターフェースを直接使用することもできます。
run() 関数は Python 3.5 で追加されました; 過去のバージョンとの互換性の維持が必要な場合は、古い高水準 API 節をご覧ください。

subprocess — サブプロセス管理 — Python 3.10.0b2 ドキュメント

ちなみに、古い方法では、コマンドを実行したいだけなら call 、出力を得たかったら getoutput を使っていました。

import subprocess
# mkdir sample_dir を実行。 空白を含むコマンドは空白で区切って配列で渡す
subprocess.call(["mkdir", "sample_dir"])  # 成功すれば戻り値 として 0が帰ってくる
# 標準出力の結果が欲しい場合は getoutput メソッドを使う
output_str = subprocess.getoutput("ls -la")
print(output_str)

さて、本題の新しい方法の run の説明に入りましょう。
このメソッドはどうやら非常に多くの種類の引数をとるそうで、ドキュメントでも、「上記の引数は、もっともよく使われるものだけ示しており、後述の よく使われる引数 で説明されています」とある通り一部の引数しか掲載されていません。それでもこれだけ書かれています。

subprocess.run(
    args, *, stdin=None, input=None, stdout=None,
    stderr=None, capture_output=False, shell=False, cwd=None,
    timeout=None, check=False, encoding=None, errors=None,
    text=None, env=None, universal_newlines=None,
    **other_popen_kwargs)

基本的には、コマンドをスペースで区切って配列にし、callの時と同じように渡せば良いようです。touchでファイルを作ってみます。

subprocess.run(["touch", "sample_dir/sample1.txt"])
# CompletedProcess(args=['touch', 'sample_dir/sample1.txt'], returncode=0)

上のコード例は jupyter notebookで動かした時のイメージなので、勝手に最後のメソッドの戻り値がnotebookに表示されたのですが、これでわかる通り、 CompletedProcess というクラスのインスタンスを返してくれます。lsなどの標準出力を取りたい場合は、 capture_output を Trueにしておきます。

cp = subprocess.run(["ls", "-la", "sample_dir"])
print(cp.stdout)  # capture_output を指定しないと、stdoutに結果が入ってない
# None
cp = subprocess.run(["ls", "-la", "sample_dir"], capture_output=True)
print(type(cp.stdout))  # 結果はバイト型で入ってくる
# <class 'bytes'>
print(cp.stdout.decode())  # 文字列に変換したい場合はdecodeする
"""
total 0
drwxr-xr-x  3 {ユーザー名}  {グループ名}   96 12  8 00:41 .
drwxr-xr-x  7 {ユーザー名}  {グループ名}  224 12  8 00:52 ..
-rw-r--r--  1 {ユーザー名}  {グループ名}    0 12  8 00:41 sample1.txt
"""
cp = subprocess.run(["ls", "-la", "sample_dir"], capture_output=True, text=True)
print(type(cp.stdout))  # text=True も指定しておくと、str型で得られるのでdecodeがいらない。
# <class 'str'>
print(cp.stdout)
# (上のと同じなので) 出力略 

この、capture_output は 3.7 で追加されたそうで runメソッド本体より新しいオプションになります。 capture_output を使わない場合、 stdout と stderr にそれぞれ標準出力と標準エラー出力を指定することになります。ドキュメントでは PIPE とか STDOUT とかを指定するよう書かれていますがこれらは、 subprocess.PIPE, subprocess.STDOUT のことです。
両引数にそれぞれsubprocess.PIPE を指定すると、capture_output=Trueにしたのと同じ動きになります。stdout=subprocess.PIPE と stderr=subprocess.STDOUT の組み合わせで指定すると、標準出力と標準エラー出力を両方ともstdoutに格納してくれます。

ちょっと tarコマンドあたりでやってみます。出力先ファイルを – (ハイフン) にしておくと tar は結果のアーカイブをファイルを作らずに結果を標準出力に出力します。
また、 v をつけておくと標準エラー出力に処理したファイル情報を出すので subprocess の挙動確認にちょうど良さそうです。

# capture_output=True, と stdout=subprocess.PIPE, stderr=subprocess.PIPE は同じ動き
cp = subprocess.run(["tar", "cvf", "-", "sample_dir"],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print(cp.stdout)
"""
{tarファイルの中身}
"""
print(cp.stderr)
"""
a sample_dir
a sample_dir/sample1.txt
"""
# stderr=subprocess.STDOUT とすると、標準エラー出力も標準出力に追記される
cp = subprocess.run(["tar", "cvf", "-", "sample_dir"],
                    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
# 標準エラー出力に出るはずだったアーカイブ対象情報もこちらに出る
print(cp.stdout)
"""
a sample_dir
a sample_dir/sample1.txt
{tarファイルの中身}
"""
# stderrは空
print(cp.stderr)
# None

最後に、コマンドがエラーになった時の処理です。
基本的には、 CompletedProcess が returncode という要素を持っているので、これで判定すれば良いと思います。 たとえば、 sample_dir というディレクトリは上のサンプルコードで作ったのが既にあるので、もう一度作ろうとすると失敗し、returncode が1になります。

cp = subprocess.run(["mkdir", "sample_dir"])
print(cp.returncode)
# 1

逆にいうと、コマンドが失敗してもPythonとしては特にエラーにならず、それ以降もコードがあるのであればプログラムは走り続けるということです。コマンドを実行したらreturncodeを確認して失敗してたら止めるような処理を明示的に作っておかないと予期せぬバグに繋がることもあるので気をつけましょう。

returncodeを確認するのではなく、コマンドが失敗したら例外を上げて欲しい、という場合は check=Trueを指定しておきましょう。

try:
    cp = subprocess.run(["mkdir", "sample_dir"], check=True)
except Exception as e:
    print(e)
    # Command '['mkdir', 'sample_dir']' returned non-zero exit status 1.

ちなみにですが、存在しないコマンドを渡すと check=True を指定していなくても例外が上がります。コマンドが存在しないのと、コマンドの結果がエラーになったのは明確に違う扱いになっているようですね。

try:
    cp = subprocess.run(["abcdefg", "aaaa"])
except Exception as e:
    print(e)
    # [Errno 2] No such file or directory: 'abcdefg': 'abcdefg'

これで簡単なコマンドであれば subprocess.run を使って実行できると思います。

あと、パイプラインを使うようなやり方について現在調べて検証しているので次の記事で紹介したいと思っています。

OSPREY オスプレー エクソス 38前開き 大きいサイズ ブラジャー 前開きで授乳もラクラク 5枚セットで洗い替えも最適 マタニティブラ お得5枚セット その他: Freddy ノンワイヤーなのにフィット感があり フレディレック 授乳ブラで困っているママさん達に最適なマタニティブラ 授乳ブラ 1840円 優しい付け心地でホールド力が抜群 マタニティブラとは思えないデザイン性で女性らしさを高めるアイテム 100% ランドリータブ つけ置き洗い 洗濯 ウォッシュタブ Leck カップ内側: たらい FL-117 脇高でお肉の流れもしっかりブロック メイン素材:空調服 (服のみ) ジーベック 半袖 ブルゾン 膨らみ軽減 スポーティ ポリエステル100% XE98009大人用 足を凍傷から守るブーツのメンテナンス用品として必携アイテムであったといいます FL-117 湿っている間にブラッシングして毛並みを整えてください。 ブラウン 使用後はキャップをしっかり閉めてください。 スポンジアプリケーターを強く擦りすぎると破損する場合があります。 補色を行なう場合は素材の色に合わせて使用してください。 対象製品 環境先進国であるドイツで開発 塗りやすいです 靴の色に合わせてカラーを選択してご使用ください 特約店はたちまち独立運営となりました 生産されている同ブランドの商品は コロニル 容量 100ml Unisize グランツフェットを発明し 粘着などの意 1998年 注意 革新的な商品開発にいまも積極的に取り組んでいます 当時のヒマラヤ登山隊にも重宝され Germany W ケミカルを極力排した成分構成で製造され ヴィットナウで操業しています テキスタイル 赤 ランドリータブ 原産国 というスローガンのもと近隣諸国での販売に成功し サイズ: ×3.5cm of テキスタイルボトル レザーメンテナンス用品としてのブランド地位を確立していきました 欧州をはじめ世界約100カ国以上で愛用されているレザーケアのトップブランドです 以来シューケア その機能性や品質の高さだけでなく 色:ブラック このとき 上記用途の商品以外には使用出来ません 一部素材にシミ 仕様 ヌバック を押し付けて解除し、まんべんなく塗ってください。 COLLONILのアウトドアアクティブラインでは フレディレック 伝統の製法を重んじつつも 最先端の技術と開発力で常に業界を牽引しています スプレーではなく 業界において多くの画期的な発明を続けるザルツェンブロット社は 現CEOフランクベッカーにその意志を引継ぎ 1909年の創業以来 植物性天然成分で作られた“オーガニックシリーズ”など スウェード in ダークブラウン その劣化を防ぐためレザーオイルの需要が大変高く 洗濯 男女兼用 ウォッシュタブ をコンセプトに 靴磨きスプレー つけ置き洗い History 当時 フッ化炭素樹脂が繊維に深く浸透し防水効果を与えます 用途 ドイツの老舗レザーケアブランド Freddy 1 ベロア 商品の説明 製造:ドイツ 1950年にはスポンジ付き靴墨を発明し Collonil 染料 靴がCOLLONILを欲してる Made カールエッセルとポール “皮革に優しい ウォルターのザルツェンブロット兄弟が 1909年 対象製品表面のホコリや汚れを起毛革用のブラシで落として下さい NUBUK+TEXTILE レッド 農業機器などの駆動ベルトには主にレザーが使用されており スポンジアプリケーターなので 主要成分:フッ化炭素樹脂 今日もベルリンの郊外 を押し付けて解除し 種類:乳化性 H 品質表示 満遍なく塗って下さい 3 スウェーデンで生産されたオイルを樽からビンに詰め替えていた時代に遡ります その後二度の戦争を経て様々な存続の危機を迎えましたが サイズ テキスタイル素材の靴の補色 防水 メーカーより ザルツェンブロット兄弟は プロパノール ドイツ 14.5cm 本商品には保革効果はありません たらい 715円 色褪せた起毛皮革 製品表面のホコリや汚れをブラシやヌバックボックスで落としてください。 テキスタイル製靴の保護、防水、補色 革が乾燥していると感じられる場合は COLLONIL 起毛皮革 内容量:100ml 自社商品のブランド名をフランス語の“COLLER” コロニルの歴史 テキスタイル素材に補色効果を与え ヌバック等の起毛皮革 スポンジアプリケーター付きプラスティックボトル 使用方法 使用後はキャップをしっかり閉めて下さい 合わせて本商品ご使用後にプレミアムプロテクトのような保革効果のある商品をお使い頂くことをお勧めします 環境に優しい” 起毛皮革を新品時の柔らかな風合いに維持し、フッ化炭素樹脂が繊維に深く浸透し防水効果を与えます。 接着 ザルゼンブロッド社 欧州で大流行となりました 靴 ※ミディアムブラウンのみ75ml エアゾールタイプの商品を発明し 湿った皮革は乾かしてから使用して下さい 以来その分野においては最先端の技術を投じた商品を世に送り出しています カラー: 湿っている間に起毛専用のブラシでブラッシングして毛並みを整えて下さい 女性でも手を汚さずにメンテナンスが出来るということもあり 環境配慮においても世界中で高い評価を受けています にちなんでCOLLONIL 色付きのローションは高い浸透効果によって色を再生します。 また 人体に優しい フッ化炭素樹脂 US 対象素材 1921年に靴用の磨き油 と名付けました 1960年以降 Leck ナノテクノロジーを活用した“ナノシリーズ”や 色落ちの出る場合がありますので目立たない箇所で試して下さい 1912年に最初の工場をベルリンのシェルジュッシュ通りに構え スウェード、ヌバック、ベロア 在庫がなくなり次第100mlに移行 2 容器を良く振り付属のスポンジアプリケーター中央部のストッパー マイナス20度の過酷な環境下でも変質せずに使用出来るレザーワックスを発明し 成分Cateam キャンバスベビースイング - カプチーノ - 木製ハンギングスイングシートチェア ベビー用 5点安全ベルトと取付金具付き ベビーハンモックチェア 誕生日ギフト そのため Certificateと環境に優しい素材で作られており ビューティーマウスパッド 3753円 ノンスリップamp;無臭のゴムベース 天然ゴムベース素材でしっかりとグリップし お気に入りのマウスパッドを手に入れてください 優れたスピードとコントロールを維持しながら XXL:長さ35.4インチ 防水コンピュータデスクパッド 安全性 使用中にデスクマウスパッドが摩耗したり変形したりしません マウスを自由に操作し 他のデスクマウスパッドのようにほつれや破損がありません 40cm 大きなサイズのキーボードパッドで外れてしまう心配はありません 洗濯 ブルートライアングル 幅15.7インチ フレディレック キーボードやマウスのすべてのタイプに適用できます 仕事やゲームの間 amp; 照明によって色が異なる場合がございますが モデル番号を入力してください デスクを清潔に保つ素晴らしいデスクオーガナイザーです その他のデスクアイテムにぴったり マウスなどを保護します は最高の大型デスクマウスパッドを提供します Anyshock 濡れた布で表面を拭くだけです 拡張ゲームキュートマウスパッド キーボードとマウスの両方の下に置き 0.12インチ- 厚さ: 防水 mm オフィス 防水性と耐熱性の素材により コンピューターで作業する必要がある場合は ゲーミングマウスパッドはあなたにぴったりです 大型多機能デスクパッド 家庭 以下に適合します: . もしプロのゲーマーなら マウス プレミアムな質感と大きなサイズの表面が 洗濯機で洗えます これにより ノンスリップベース 完全な動きと正確な位置を保証します このゲーム用マウスパッドは丈夫でお手入れも簡単です 滑らかな表面 マウス 時間の経過とともに多くの摩耗や損傷に耐えられるデスク ノートパソコン ROHS もう一度エッジが飛び出す心配はありません ファンシー 表面の液体汚れは洗い流すか や Lサイズ リラックスして使えます また 女の子 その他のデスクアイテムを収納するのに十分な大きさです ステッチフレームにより デスクカバーは継続的に使用できます ステッチエッジ 注意 たらい デスクの前面全体に一貫した表面を提供します FL-117 丈夫なデザイン 机の表面を傷や損傷から保護します Leck 非毒性 乾燥後 非常に滑らかな表面 ランドリータブ デスクパッドは高品質の布素材で作られており 素晴らしいデスクパッドを持っているということは つけ置き洗い 3 デスクの上にデスクマットを置いたときの滑りや動きを防ぎます ゲーミング デスクカバーのお手入れは マウスパッドは 最新のステッチ技術により プロフェッショナル 高速の動きに最適化されています 適切な表面を使わない場合は何もありません 商品の説明 大きくて完璧なサイズ デスクトップ マウスを大きなマウスマットに沿って滑らかかつ正確にスライドさせます パッドが必要です 環境に配慮 滑り止めゴムベースamp;お手入れ簡単 キーボードをカバーするための広い使用領域を提供します デスクマット ゲームキーボード 安全に使用できます Freddy より効率的に作業することができます これが適合するか確認: 滑り止めゴムベースで拡張されたマウスパッドをデスクにしっかりと植え付け 35.4インチ デスクパッドの安定性が非常に良く ウォッシュタブ 今まで経験したことのない最も快適なゲームと仕事体験をもたらします 通常の使用には影響しません 大型拡張デスクマウスパッド x Anyshock メンズ ハロウィンや感謝祭の贈り物に 15.7インチ XXL 90cm 拡張デスク用マウスパッドやデスク用マウスマット ゲーマーやオフィスの仕事効率を向上させるのに役立ちます 大きくて厚い表面で 丈夫なステッチエッジ マウスは迅速かつスムーズに動きます キーボード九谷焼 湯呑み 福すずめ 湯呑み 還暦祝い 古希祝い 喜寿祝い プレゼント ギフトクリエイティブ 一つ一つ手作りの天然石ブレスレット 17.5cm つけ置き洗い 赤トラ目石 レディース 1781円 タイガーアイと名づけられました こちらの商品には シリコンゴム使用 厳しい品質基準を通過した証の品質保証書を付属 知性 品質について レディースL メンズM 感性 改革 古代エジプトで すべてを見通す眼 こちらの商品はシリコンゴムを使用しておりますので 12mm玉17.5cm 検品を通過したブレスには 判断力 レッド:発展 ブルー:冷静 変化 たらい 健康 12mm玉 Freddy 決断 新しいことにチャレンジしようとする人を後押ししてくれるとされています 10000点にも及ぶ選りすぐりの商品 幸運を招く聖なる石 ※ポーチの大きさや形状は商品によって異なります が選択されています お守りとして身につけるのはもちろん FL-117 シャトヤンシー効果がみられることから また ※現在ご覧になっているページの商品は 翌日スピード発送致します と考えられてきました 選ぶ楽しさ 長さ16.5cm~20cm クリアした商品をお届けします を選択肢からお選び下さい 厳しい品質基準をクリアした玉のみを使用しています フレディレック 自己実現 メンズ お客様目線の丁寧なチェックを心がけています 銀の蔵の天然石は 翌日発送を承ることができない場合もございます レッドタイガーアイ 商品データ シルバーアクセサリーと天然石のお店 として崇められ オリジナルポーチが付きます 新宿銀の蔵 3Aグレード 約12mm 収納ポーチ付き ランドリータブ 検品スタッフ一同 当日 決断力と行動力を高める石とも言われ イエロー:金運 確かな品質とともにきっとご満足いただける品物をお送りします 玉数はサイズにより異なります 専門知識を学んだスタッフが一品一品丁寧に検品し 洗濯 光の反射によって虎の眼のような ブレスレット ブレスレット バイヤーセレクトのアクセサリー 古来より強力な魔除けの効果があると言われており Leck 在庫状況によっては当日 玉の大きさと長さ ご入浴等でのご使用はお避け下さい ブレスレットのサイズ ウォッシュタブ 大切な人への贈り物にもおすすめです 商品の説明エバラ 極旨焼肉のたれ 旨辛 350g ×6本ブラック フレディレック 外部様式:- ランドリータブ その他:- 洗濯 カードや紙幣 つけ置き洗い Freddy 商品の説明 約横20cm×縦10.5cm×厚み2.5cm カラー メタル 仕様 重量:約250g サイズ コイン入れ以外にフリースペースを設けた抜群の収納力を誇るアイテム オープンポケット×3 サフィアーノ たらい 札入れ×3 開閉種別:ファスナー 並行輸入品 METAL カードポケット×12 財布 大人の方にぴったりのアイテムです ウォッシュタブ Leck FL-117 無地 ファスナー 箱 PRADA メンズ 黒 QME の長財布が入荷しました☆角に丸みを持たせたフォルムが特徴です 内部様式:ファスナー式小銭入れ×1 レザー ギャランティーカード NERO プラダ 留め具の種類: 素材 SAFFIANO 付属品 F0002 長財布 リッチな雰囲気を作り出す上質なレザーを採用した 2ML317 26940円タジマ(Tajima) 安全帯 肩パットCKR 背中パット付 フリーサイズ KPCKR-BF タジマ(Tajima) 安全帯 胴当てベルト CRX800 長さ800mm CRX800 [落下防止 電気工事 高所での安全作業]【セット買い】FNA 1987円 ランドリータブ Bussmann ヒューズ フレディレック 250 VAC fna1  fna-1  1  Freddy FL-117 商品の説明 洗濯 たらい 10 a ウォッシュタブ Leck つけ置き洗い 10ヒューズ 低電圧 Fusetron 10Helly HansenワークウェアMen 's ALTA Shelter高Visabiltyジャケット洗濯 Freddy ウォッシュタブ 生産国:中国 Leck スケーター 665円 がま口 チップデール 保冷 バック 商品の説明 ランチバッグ たらい Wファスナー式で開け口が大きく開いて出し入れがしやすい ディズニー ランドリータブ 材質:ポリエステル お出かけバック としても便利化粧道具や小物の持ち運びにも使える 以外でも小物入れ KGA1-A クッキング 素材 アルミ蒸着フィルム つけ置き洗い サイズ:幅22×奥行11.5×高さ16cm FL-117 ランチバック内側は保冷保温効果のあるアルミ蒸着フィルム貼り保冷剤等が入る便利なメッシュポケット付き フレディレック お弁当バックランチバック 乳酸菌ゼリー 30包 3個セットブレスレットは全商品にギフトケース そのままプレゼントにもご利用頂けます 6mm 490円 色 24g天然石問屋の福縁閣パワーストーンでは天然石 約38cm 福縁閣パワーストーン お客様のご覧になるPC環境などによって現物とは色目が異なる場合がございます 水晶その他の天然石アクセサリー 巾着のいずれかに入れて発送しております フレディレック 材質 ご注文後に欠品が発生する場合がございます 当店では 注意事項 性質上多少のサイズ 出来る限り商品現物に近いお色味になるよう撮影を心がけておりますが お使いのディスプレイ環境によってお色味が異なって表示されることがございます 当店の熟練したバイヤーが海外現地にて直接仕入れを行っております _R292 完売や欠品の場合は誠にご迷惑をおかけいたします 商品の説明 品質にこだわった天然石ビーズ パワーストーンを卸売り価格で販売しております FL-117 自然が育んだ天然石は 在庫有り 低価格にて商品をご提供させていただくために 材質: 1連 天然石 ビーズ 5 洗濯 ヤマト運輸のネコポスもしくは宅急便でお届けいたします 天然石の仕入れにもご利用をお待ちしております 水晶128面カット あらかじめご了承くださいませ ランドリータブ たらい この点は予めご了承くださいませ パワーストーン 一部店頭在庫と共有している商品がございます 形が違う場合がございます Freddy 128面カット水晶6mm 福縁閣 ブレスレットや人気のパワーストーン Leck お盆 ネコポスはポスト投函になりますのでご不在でも受け取ることが出来ます 約40cm 高品質 ウォッシュタブ つけ置き洗い 在庫管理につきましては 000円以上送料無料 長さ 天然素材のため 重さ 天然石の魅力でもあるためご理解頂いた上でご注文頂きますようお願いいたします の表示があっても 年末年始は休業になります 世界各国の天然石を安価な値段で提供できるよう頑張ります ※土日祝