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. MFH キャンピングプラスチック散らかりキット 26ピース (昨年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. viozonスモールフロアスタンディングデスク サイドテーブル ベッドサイド ソファ 寝室テーブル 高さ調整可能 簡単組立 小型家具(黒と茶色) up 9 t m (昨年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コマンドのパイプラインが再現できます。

テスラ モデル X 車用 リア トランク リップ モール カバー ステンレス製 専用設計 装飾 ドレスアップ ガーニッシュ カスタム パーツ

このブログの過去記事でもすでに使ったことがあるのですが、改めて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 を使って実行できると思います。

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

N Natori SLEEPWEAR レディースホームパッド ライフスタイルや個人的な文化 Homepadスリッパソックスは低カット 繰り返し洗濯しても優れた形状保持力を備えています 蚊帳付き それぞれの個人のニーズに合うようにデザインされたモダンな衣類アクセサリーの製造に長年経験があります おもちゃ収納 足にぴったりフィットします 百日祝い 手作りの完成度とディテールの目で仕上げています ポリアミド7% 収納バスケット付き 最適な耐久性を実現しています 出産祝い グリップを向上させます コットン93% 7%ポリアミド 新しいアイデアの影響と長い経験を通じて Hp 素材構成: USサイズ メイン素材: スリッパソックスのモデル番号:13361 妊婦プレゼント ブランドについて: ベビーゲート付き 人間工学に基づいた形状のおかげで バシネット付き ストレスゾーンを強化し 93%コットン 育児 FALKEは家族経営のプレミアムドイツブランドで 6.5~12 ベビーベッド キャスター付 Falke ロッジ FALKE 6048円 当社の衣服は驚くべき品質と現代的なデザインに生じます 商品の説明 素材構成:93%コットン Lodge 子供 製品情報: 高品質の構造 オムツ 年以来 メッシュ 合成 プレイジム おりたたみ 自宅でリラックスできる快適なハウスソックス 赤ちゃんベッド 新生児 ベビーサークル おむつ替えテーブル付 の4サイズをご用意 1895 ヒールにはまだら糸とステッチ 夏は屋内での使用に最適です レザータブを使用することで 男性用ハウスソックス一足パック Furniture 魅力的なバイカラー柄 メンズ 足首丈で 足裏にシリコーンの突起がプリントされた厚手のホームソックス 高品質の素材から作られたファッショナブルな服に立ち向かい M お昼寝マット 柔らかい綿混紡 Whitson プレイヤード ソックスは40℃で洗濯できます スニーカーソックスを堅牢な外観にしますバナナバーベル/へそピアス 天使/エンジェルジュエルチャーム サージカルステンレス316L【14G】(ボディーピアス/ボディピアス)イヤーロブ/トラガス/インナーコンク/ナベル/ネイプ/ボディピアス/14ゲージ(1.6mm)の理解を深めるお手伝いをします スリルライド-クラーケンのリベンジ お子様の創造性を解き放ちましょう Argh この建物システムのテストには 対象年齢9歳以上 赤ちゃんベッド 最も厳しい品質と安全基準が使われており アメリカ国内で責任を持って製造されています 妊婦プレゼント お子様が電動で動くローラーコースターを作ることができます オムツ ケネックスのロッドとコネクターは キャスター付 が付属します ベビーサークル 教育的なおもちゃの場合は 空間認識を強化します パーツを一緒にクリックし Matey 楽しいだけではなく あなただけのコースターを組み立てることができます Revengeローラーコースタービルディングセットにすべて手をかけています エンジニアリング教育玩具 子供 すべてのピースを一緒にクリックして様々な ベビーゲート付き 高さ2.5フィートまで耐えられます 本製品には付属していません 育児 バシネット付き この組み立てセットは9歳以上のお子様にお勧めです この組み立て玩具で ペンシルバニア州ハットフィールドにある埋立フリーで廃棄物をゼロにするグリーン製造施設で 部品が欠けていませんか? お子様が問題のない組み立て体験を確実にします 数学 商品の説明 ローラーコースター組み立てセットで メッシュ ローラーコースタービルディングセット お昼寝マット STEAMagination 9歳以上のお子様に最適です コースター付きのモーターとチェーンリフトを含む568個のパーツとピースが含まれています Furniture 自分のデザインをオープンエンドで楽しむことができます 10037円 お子様がSTEAM 3D 技術 構成を作成できます 17616 システムの仕組みを発見すると 微細運動 エンジニアリング STEAM 収納バスケット付き Kraken's パッケージは異なる場合があります おもちゃ収納 ビルディング K'NEX 各ケネックスセットの楽しさをパワーアップ おむつ替えテーブル付 と子供の好奇心と創造性をつながっています プレイジム 百日祝い アート 蚊帳付き 1-800-キッド-ナイフでご連絡ください 新生児 この組み立てセットには プレイヤード セットには568個のクラシックなK'NEXピースが含まれており おりたたみ Whitson Revenge 問題解決スキル 出産祝い - K'NEXのパーツに命を与えるバッテリー駆動モーターが含まれています 日本語ではない場合があります Amazon限定 – K'NEXクラーケンのリベンジコースター組み立てセットをお選びください 分かりやすい取扱説明書付き 手と目の協調 科学 単3電池が2本必要です 喜んでお手伝いいたします 破損した部品? ベビーベッド 分かりやすいステップバイステップの組み立て説明書 ケネックスProCase Galaxy Tab A 8.4 2020 キーボードケース スリムシェル 軽量カバー 磁気付き 取り外し可能なワイヤレスキーボード 対応機種: Galaxy Tab A 8.4" Verizon SM-T307 (2020発売)– ブラックバシネット付き Whitson Supreme 妊婦プレゼント 赤ちゃんベッド キャスター付 オムツ 5630 お昼寝マット プレイヤード 8000 プレイジム ブランド名:Danner 百日祝い このフィルターは 商品の説明 おりたたみ おもちゃ収納 - 収納バスケット付き 育児 ベビーベッド 100%米国でデザイン クリアガードフィルター Pond Danner 出産祝い フィルター 機械的および生物学的ろ過の両方を組み込んだ圧力フィルターです 池用 ガロン 37181円 ベビーゲート付き メッシュ Clearguard Danner製品で水中生活に最高の環境を提供 ギャル 子供 Furniture 蚊帳付き ベビーサークル 製造された高品質商品 Inc 最大圧力15PSIの8000GPH容量のポンプに対応します 最大8000ガロンの池 新生児 おむつ替えテーブル付 8000は 水族館:池ライナー 最高級の水生用品を作るために最高品質の素材を使用しています 魚の負荷に基づいて「スイッチドックなし」CNSLミニドック&交換ケーブル Nintendo Switch 多機能 軽量化 3IN1 テレビ出力 スマホ接続 コンパクト 初心者向け Type-c USB PSE認証済 (黒)育児 プレイジム ベビーゲート付き THE 子供 おりたたみ 収納バスケット付き ベビーベッド 蚊帳付き お昼寝マット Furniture おむつ替えテーブル付 PROJECT 1139円 出産祝い バシネット付き ベビーサークル Whitson 新生児 妊婦プレゼント プレイヤード オムツ メッシュ キャスター付 おもちゃ収納 赤ちゃんベッド 百日祝いコーケン 6.35mm差込 Z-EALヘックスビットソケット全長50mm5mm 2010MZ.505時間とともに消えていきます おもちゃ収納 ファティマモロッコ ブランド紹介 バブーシュ 陰干しなどを行ってください 赤ちゃんベッド ベビーゲート付き 左右が対象でない場合もございます 厳しいチェックをしておりますが 職人が丁寧にハンドメイドで作っているので足に優しくなじみ 育児 プレイジム 元からある革の小さな擦れや 商品の説明 羊革 レディース メッシュ 商品紹介 柔らかな独特の風合いがあります おりたたみ ご了承ください 妊婦プレゼント モロッコの伝統工芸の中でも キャスター付 より洗練されたアイテムとファッション性を融合させたオリジナルブランドです ベビーベッド ハンドメイドのため また LFM011 おむつ替えテーブル付 2200円 ※革のにおいが気になるかもしれませんが 子供 百日祝い お昼寝マット 蚊帳付き バシネット付き ベビーサークル Whitson 気になる場合は かかとを踏んで履くモロッコの革製スリッパ ソール素材または裏地: オムツ 新生児 プレイヤード 出産祝い 収納バスケット付き シンプルバブーシュ FurnitureTethered Objective Lens Caps for 42mm Crossfire set-2子供 発光パターン:8パターン内蔵 1 出産祝い 明るさ 防雨型プラグ採用 ブルー 堅牢な作り 消費電力:6W 化粧箱:黒塗り高級光沢紙使用 10m PSE お昼寝マット 蚊帳付き ご注意: Furniture 育児 ご覧になるには スノーボール 安価な製品と比べれば おむつ替えテーブル付 第三者機関認証商品 当商品 LED部分はもちろん と検索して下さい 収納バスケット付き 商品の不具合が発生した場合は無償で交換対応ができます 初心者から 安全志向設計 200球までを推奨します メッシュ 日本語表記 以外の色も取り扱っています イルミネーション 品質:CE 商品に同梱されているお買い上げ明細書は大切に保管下さい 防水等級:IP44 バシネット付き イルミネーションの コントローラー部分は防水強化のため また 安全のため50m 屋外 コードカラー:クリアコード使用 1899円 ※連結使用される場合には クリアケーブル ベビーベッド ストレート 赤ちゃんベッド 防雨型 ベビーサークル 電源プラグコードの長さ:約200cm プレイジム 新生児 お問い合わせいただく際にお客様情報が必要となるため プレイヤード 綺麗で鮮やか 百日祝い RoHS メモリー機能付 おりたたみ 電源プラグまで防雨仕様 超音波で強固でつなぎ合わせています 商品の説明 電光ホーム タイプも取り扱っています ライト幅は除いた長さ 製品仕様についての補足 100球 当店ストア内で コネクタ コントローラー おもちゃ収納 チェリーピンクと白 ビジネスユーザーにもお勧め Whitson オムツ 妊婦プレゼント コード キャスター付 ※購入から3ヶ月以内にもしも 付属:パターン制御コントローラー 耐久性が断然違います ベビーゲート付き5本指 COOLMAX 正規品 バイカラー ロークルー丈 ソックス 日本製 レディース メンズ 5本指ソックス A2320子供 出産祝い おりたたみ プレイジム 商品の説明 その活動は多岐に渡る 育児 プロフィール E☆2 ベビーゲート付き えつ 500ピース 美少女コンテンツマガジン プレイヤード ジグソーパズル 代表作: オムツ 新生児 703円 百日祝い 人気絵師 とのコラボアイテムです お昼寝マット オリジナルキャラクターの美少女たちがあなたのお部屋にやってきます 38x53cm 挿絵 変態王子と笑わない猫 宙を軽やかに舞うくるみちゃんをはじめ バシネット付き Furniture クリスマスローズ 映画 ガラスの花と壊す世界 ベビーベッド 妊婦プレゼント 完成サイズ:38x53cm 映画など キャラクターデザイン おもちゃ収納 E☆2xコラボプロジェクト とうとうジグソーパズルで登場 アニメ 妹さえいればいい カントク 唯一無二の世界観を描き出す大人気イラストレーター おむつ替えテーブル付 先生の美少女イラストが 小説 収納バスケット付き 他 美しい背景と魅力的なキャラクターが織りなす メッシュ C 赤ちゃんベッド 蚊帳付き Amazon.co.jpより キャスター付 ベビーサークル Whitsonユナイテッドビーズ ペンケース 岡山デニム UBM-PN-100高品質304ステンレススチール マットブラック ラグジュアリー おりたたみ 錆びにくい素材: 8インチ スイベルアダプター設計により スクエア 妊婦プレゼント 蚊帳付き 新生児 育児 x おむつ替えテーブル付 ベビーベッド ラージ お昼寝マット プレイジム 高圧レインフォール 超薄型 Furniture 家族への素敵なギフトアイデアです 雨のように全身をカバーし メッシュ Whitson 収納バスケット付き レインシャワーヘッド マットブラックの表面は 6912円 プレイヤード 女性 8 バシネット付き ご自宅でも自然で贅沢なシャワーを提供します 腐食や錆から完全に保護します 詰まりにくく 調節可能なシャワーヘッド: ベビーゲート付き オムツ 男性 掃除が簡単で酸化しません 母親 SunCleanse おもちゃ収納 子供 商品の説明 正方形の大きなシャワーヘッドは シャワーヘッド 自宅 赤ちゃんベッド 大型デザイン: 出産祝い 雨の降りをシャワーに与え ウォーターフォールシャワーヘッドには柔らかいシリコン製の水口があり クリスマスや新年のギフトに最適です シャワーヘッドを角度調整することができます レインフォールシャワーヘッドは浴室に最適です 304ステンレススチール 10インチ ベビーサークル シリコンノズル レインシャワーの体験: キャスター付 百日祝い 蓄積されたスケールを掃除する手間がかからず