2014/05/02
今日のワンライナー その1:配列から最も要素数の多い要素を取りだす
また続くかどうか不明の新シリーズ。今日書いたワンライナーを記録していきます。
今回は、配列に含まれる要素のうち、もっとも出現頻度の多いものを調べる方法です。たとえば、0, 0, 0, 1, 0, 3, 0, 2, 0, 2という配列がある場合、「0」は6個含まれており最も要素数が多いので、この場合「0」を出力します。
ワンライナーは以下のようになります。(配列変数の宣言を合わせると2ラインですけど)
$array = 0, 0, 0, 1, 0, 3, 0, 2, 0, 2 $array | group | sort Count -Descending | select -First 1 | select -Expand Name
今回のケースのように、「配列内に同一の要素が含まれており、要素ごとに纏める」という手順が必要なときはGroup-Objectコマンドレット(エイリアス:group)の出番です。
$array | group とすると、
Count Name Group ----- ---- ----- 6 0 {0, 0, 0, 0...} 1 1 {1} 1 3 {3} 2 2 {2, 2}
のような出力が得られます。Count=出現回数、Name=要素、Group=該当する要素のリスト です。なのでこの出力だけでお題の解答である「0」が確認できます。あとはこの出力から求める値を抽出するだけです。
Group-Objectコマンドレットの出力はGroupInfoというオブジェクトです。そう、これもオブジェクトなので、他のオブジェクト同様、プロパティ情報を保ったまま後続パイプラインに渡すことができます。
まずSort-Objectコマンドレット(エイリアス:sort)を用いてGroupInfoのCountプロパティの値で降順ソート(-Descendingパラメータ使用)をかけます。するとCountプロパティが一番大きいGroupInfoオブジェクトが一番最初の要素となります。(今回の場合はたまたまソート前後で最初の要素が同じでしたが)
次にSelect-Objectコマンドレット(エイリアス:select)に-First 1と指定することで一番最初のGroupInfoオブジェクトのみ取得します。最後に、GroupInfoオブジェクトのNameプロパティの値だけを取りだすのにSelect-Objectコマンドレットに -ExpandPropertyパラメータを指定します。
ところでGroupInfoオブジェクトのGroupプロパティ、どうせNameと同じものがCount分列挙されるだけじゃないか何の意味が?と思われるかも知れないので補足です。Group-Objectコマンドレットには-Propertyパラメータ(位置パラメータなのでパラメータ名は省略可能)が定義されており、指定すると任意のプロパティ値に基づいてグループ化してくれます。
たとえば
dir | group Extension
とすると、カレントディレクトリに含まれるファイルが拡張子別にグループ化され、以下のような出力が得られます。
Count Name Group ----- ---- ----- 1 .gadget {twitterpost.gadget} 59 .vbs {7zip_fix_archive.vbs, 7zip_store_each.vbs...} 72 .ps1 {cddrive.ps1, clipboard.ps1, cmdlets.ps1...}
このようにGroupプロパティの中身は、指定プロパティ値を持つ要素のグループとなっていることが分かると思います。
-Propertyパラメータには集計プロパティを指定することもできるので、
1..10 | group {if($_ % 2 -eq 0){"偶数"}else{"奇数"}}
Count Name Group ----- ---- ----- 5 奇数 {1, 3, 5, 7...} 5 偶数 {2, 4, 6, 8...}
みたいなこともできたりします。
ちなみにGroup情報が不要であるときは、Group-Objectコマンドレットに -NoElementパラメータを付与すると出力を抑制できます。(この場合、出力はGroupInfoオブジェクトではなく、GroupInfoNoElementオブジェクトとなる)
なんか後半はGroup-Objectコマンドレット特集みたくなってしまいました。ではまた次回。
2014/04/20
集計プロパティの簡易指定
Select-Object、Format-Table、Sort-Objectなど、-Propertyパラメータを持つコマンドレットには、プロパティ名を指定する以外にも「集計プロパティ」という連想配列を指定することができます。
集計プロパティを利用すると、オブジェクトが持っていないプロパティを動的に作成し、その値を表示することができるようになります。
たとえば、dir (Get-ChildItem)の出力するファイルリストに、テキストファイルとして開いた場合の1行目の記述(Textプロパティとする)も合わせて表示したい場合は以下のようにします。
dir | select Name, Extension, @{Name="Text"; Expression={$_|gc|select -First 1}}
出力は以下のようになります。
Name Extension Text ---- --------- ---- test1.ps1 .ps1 param([Parameter(ValueFro... test2.ps1 .ps1 $x=1231 test3.ps1 .ps1 Start-Transcript test4.ps1 .ps1 Get-Content .\gacha_log.t...
ここで、@{Name="Text"; Expression={$_|gc|select -First 1}}と指定している部分が集計プロパティです。連想配列のキーとしてプロパティ名を表すNameと、スクリプトブロックを指定するExpressionを含めます。なお、キー名のNameはN、ExpressionはEと省略表記できるので、
dir | select Name, Extension, @{N="Text"; E={$_|gc|select -First 1}}
と書くこともできます。
ここまではヘルプにも載っていることなのでご存知の方も多いと思います。ただ、連想配列を指定してごにょごにょ、というのは(特にインタラクティブ実行時には)正直だるいです。そこでもっと楽な書き方はないものかと思って何気なく、
dir | select Name, Extension, {$_|gc|select -First 1}
とプロパティとしてスクリプトブロックをそのまま書いたら、普通に通りました。これ、知ってました? 私は知らなかったです。しかしv2環境で実行しても動いたので前からあったんですね。なぜヘルプに載ってないのか…。それともどこかに載ってるんでしょうか…。
ただしこの方法だと、集計プロパティでプロパティ名(Nameキー)を指定しない場合と同様、以下のようにプロパティ名がスクリプトブロックの定義そのままになります。
Name Extension $_|gc|select -First 1 ---- --------- --------------------- test1.ps1 .ps1 param([Parameter(ValueFro...
そのため、コマンドの出力を変数に入れて利用する場合などはこの方法は不適です。ですがインタラクティブ実行時で、結果表示の見た目よりも効率を重視する場合なら、この方法はお勧めです。
2013/03/29
Twitterの「全ツイート履歴」を分析してみよう
はじめに
Twitterブログ: 日本の皆さんにも「全ツイート履歴」が使えるようになりました の記事のとおり、自分の全ツイートデータをダウンロードする機能がTwitterで利用可能になっています。
ダウンロードされるzipファイルには、ツイートを表示するためのHTML、JavaScriptファイルのほか、CSV形式のデータ(tweets.csv)も含まれています。CSVファイルの処理といえばPowerShellが得意とするところです。このファイルを読み込んで、PowerShellで自分のツイートを分析してみましょう。
準備
具体的にダウンロードする方法は上記記事を参考にしていただいて、まずはダウンロードしたzipファイルからtweets.csvを解凍し、PowerShellのカレントディレクトリをtweets.csvのあるフォルダに移動させておいてください。
毎回CSVを読み込むと時間がかかるので、まず以下のようにしてImport-CsvコマンドレットによりCSVファイルを読み込み、変数にオブジェクトとして入れておきます。
$tweets = Import-Csv tweets.csv
なお私の総ツイート数は4万ほどで、tweets.csvは10MB程です。これくらいの容量だとそのままでもまずまずまともな速度で分析が可能ですが、何十万ツイートもしていらっしゃるTwitter廃人マニアの方は、適宜ファイルを分割するなどして対処願います。
CSVファイルのヘッダ行は
"tweet_id","in_reply_to_status_id","in_reply_to_user_id","retweeted_status_id","retweeted_status_user_id","timestamp","source","text","expanded_urls"
となっています。Import-Csvコマンドレットはデフォルトでは1行目を出力オブジェクトのプロパティ名とするので、データ行の1行がtweet_idプロパティ等を持つオブジェクトとして読み込まれ、$tweets変数にはそのオブジェクトの配列が格納されることになります。
ツイート抽出/検索
一番最初のツイートを表示
PS> $tweets | select -Last 1 tweet_id : 948090786 in_reply_to_status_id : in_reply_to_user_id : retweeted_status_id : retweeted_status_user_id : timestamp : 2008-10-06 10:54:10 +0000 source : web text : はぐれメタルがあらわれた! expanded_urls :
Select-Objectコマンドレット(エイリアスselect)はオブジェクトの絞り込みに使います。このCSVファイルではツイートの並び順がタイムスタンプの降順なので、最初のツイートは一番最後の行となります。
直近5ツイート表示
PS> $tweets | select -First 5 | fl timestamp,text timestamp : 2013-03-21 17:02:23 +0000 text : Need for Speedがなんか懐かしい。初めて買ったPCに体験版がバンドルさ れてた記憶がある。 timestamp : 2013-03-21 17:01:23 +0000 text : そいえばEAのシムシティ不具合お詫び無料DL特典、何選ぼうかなあ。シム シティ4あるけど英語版という噂だし2013やった後につらいもんがありそう 。 timestamp : 2013-03-21 16:45:09 +0000 text : というわけでシムシティ大好きなんで、私の街を返してください… ...
Format-Listコマンドレット(エイリアスfl)を使うと必要なプロパティ値のみ抽出してリスト形式で表示できます。
文字列で検索
PS> $tweets | where {$_.text -match "眠い"} | fl timestamp,text timestamp : 2013-03-05 10:46:39 +0000 text : 眠いのってもしかしてアレルギールの副作用かも。蕁麻疹がひどいときし か飲んでないんだけどねえ timestamp : 2013-03-05 05:42:18 +0000 text : なんでこんなに眠いのかな timestamp : 2013-03-04 07:44:18 +0000 text : 眠いなあ ...
Where-Objectコマンドレット(エイリアスwhere)を使うとオブジェクト配列のうち特定条件のもののみ抽出できます。ここではツイート本文(textプロパティ)に"眠い"という文字列が含まれているものを抽出しています。どんだけ眠いんですか私は…
2009年のツイートのみ表示
PS> $tweets | select @{L = "timestamp"; E = {Get-Date $_.timestamp}},text | where {$_.timestamp.Year -eq 2009} | sort timestamp | fl timestamp,text timestamp : 2009/01/01 0:01:08 text : あけおめ! timestamp : 2009/01/01 0:16:31 text : 2chとついったー強いなーmixiしんでた timestamp : 2009/01/01 13:37:50 text : 家族でおせちをたべた。おいしかった ...
もちろん本文に含まれる文字列以外にも、timestamp(ツイート時刻)で抽出するなどもできます。ここではtimestampがGMTで分かりづらく、かつ文字列のため扱いづらいので、Select-Objectに集計プロパティを指定してDateTime型に変換しています。Format-ListやSelect-Objectに指定する集計プロパティの書式は、@{L="ラベル";E={値を返すスクリプトブロック}}のように連想配列で指定します。LはLabel、EはExpressionのように省略せずに指定してもOKです。
集計プロパティはあんまり解説を見かけないですけども、オブジェクトを処理するコマンドレットの多くで利用可能できわめて重要なので覚えておくと良いと思います。
よるほ成功ツイート
PS> $tweets | where {(Get-Date $_.timestamp).ToString("HH:mm:ss") -eq "00:00:00"} | fl @{L = "timestamp"; E = {Get-Date $_.timestamp}},text
応用でこんなんもできます。0:00:00ちょうどのツイートを抽出します。私はかつてよるほ成功したことがないので結果は何も返ってきませんけど。
ツイート中のURLリストを作る
PS> $tweets | where {$_.expanded_urls} | select -expand expanded_urls http://ja.wikipedia.org/wiki/%E5%B2%A1%E7%B4%A0%E4%B8%96 http://htn.to/4oxXDN http://guitarrapc.wordpress.com ...
whereによる抽出を応用するとこういうこともできます。なお、expanded_urls列は本文中のURLが複数含まれているとそれらは,で区切られるため、可変長の行となります。Import-Csvコマンドレットはこのような可変長なCSVに対応していないので、複数URLがあっても最初の1つのみ取得します。それとexpanded_urlsが追加されたのはt.coによるURL短縮が始まってからなので、昔のツイートにこの値は含まれていません。
ツイート数統計
月別ツイート数表示
PS> $tweets | group @{E = {(Get-Date $_.timestamp).ToString("yyyy/MM")}} -NoElement Count Name ----- ---- 432 2013/03 413 2013/02 248 2013/01 741 2012/12 497 2012/11 791 2012/10 659 2012/09 ...
ツイート分析と言えばやはりツイート数統計を取ることから始まるでしょう。統計を取るにはGroup-Objectコマンドレット(エイリアスgroup)が使えます。ここでもグループ化キーとして集計プロパティを指定してやります。ツイートの「年/月」を文字列化し、それが同じツイートでグループ化することで、月別ツイート数の統計が表示できるわけです。
時間帯別ツイート数表示
PS> $tweets | group @{E = {Get-Date $_.timestamp | select -expand Hour}} -NoElement | sort @{E = {[int]$_.Name}} Count Name ----- ---- 2369 0 1630 1 1137 2 ... 2270 23
やり方としては先ほどのとほぼ同じです。Select-Object -ExpandPropertyはパイプライン入力でオブジェクトのプロパティ値を取得できるのでよく使います。ちなみにPowerShell 3.0だと「$obj|foreach プロパティ名」でも取れますね。
Sort-Objectコマンドレット(エイリアスsort)でもソートキーとして集計プロパティを指定できます。ここではNameプロパティ(グループ化キーの値)をintに変換したものをキーにソートしています。
曜日別ツイート数表示
PS> $tweets | group @{E = {Get-Date $_.timestamp | select -expand DayOfWeek}} -NoElement | sort @{E = {[DayOfWeek]$_.Name}} Count Name ----- ---- 4939 Sunday 5164 Monday 5463 Tuesday 5164 Wednesday 5563 Thursday 5992 Friday 6331 Saturday
これもやり方としてはほぼ同じ。ソートキーはDayOfWeek列挙体にキャストしてちゃんと曜日順に並ぶようにしてます。
ツイート数計測
総ツイート数
PS> $tweets | measure Count : 38616 Average : Sum : Maximum : Minimum : Property :
ここからはツイート数の計測をしていきます。単純にツイート総数を取るだけならMeasure-Objectコマンドレット(エイリアスmeasure)を使うだけでOKです。Averageなどは対応するスイッチパラメータ(-Averageなど)を指定すると計測されますが、この場合は元オブジェクトが数値ではないのでエラーになります。
ツイート文字数分析
PS> Add-Type -AssemblyName System.Web PS> $tweets | where {!$_.retweeted_status_id} | select @{L = "TextLength"; E = { [System.Web.HttpUtility]::HtmlDecode($_.text).Length}} | measure -Sum -Maximum -Minimum -Average -Property TextLength Count : 37718 Average : 48.7322233416406 Sum : 1838082 Maximum : 140 Minimum : 1 Property : TextLength
ツイート文字数を計測するとき、元のオブジェクトにはツイート文字数を返すプロパティはないので、Select-ObjectコマンドレットにTextLengthという集計プロパティを指定して新たに作ってしまいます。
Measure-Objectコマンドレットは-Propertyパラメータにより対象オブジェクトのどのプロパティ値を計測するか指定できます。そしてスイッチパラメータを全部有効にすることで、平均、合計、最大、最小値を計測しています。私の総ツイート文字数は183万です。
なお、リツイートの場合はretweeted_status_idにリツイート元のツイートIDが入るので、このIDがあるものはWhere-Objectで除外してます。またツイート本文の<や&などはHTMLエンコードされたものがtext列に格納されているので、HttpUtilityを使ってデコードしてから文字数をカウントしています。
通常ツイートとRTの比率
PS> $tweets | foreach { $TweetCount = 0; $RTCount = 0 } { if($_.retweeted_status_id){ $RTCount++ }else{ $TweetCount++ } } { New-Object psobject @{ AllCount = $tweets.Length; TweetCount = $TweetCount; RTCount = $RTCount; RTRatio = $RTCount/$tweets.Length } } Name Value ---- ----- RTCount 898 TweetCount 37718 AllCount 38616 RTRatio 0.023254609488295
Measure-Objectコマンドレットは計測方法を指定することはできないので、独自の計測を行う場合はこんな感じでコードめいたものを書く必要が出てくるかと思います。RT率たったの2%か…ゴミめ…
ForEach-Object(エイリアスforeach)は1個のスクリプトブロックをパラメータに指定するとprocessブロック相当の列挙部分を実行しますが、このように3個指定すると、それぞれbegin(初期化処理)、process、end(終了処理)ブロックに割り振られます。
ここではbeginブロックで変数初期化、processブロックで通常ツイートとリツイートを加算、endブロックで計測値をPSObjectに格納して出力してます。ちなみにPowerShell 3.0ではカスタムオブジェクトを作る場合は「[pscustomobject]@{連想配列}」で書くほうが楽です。
お前は今まで寒いと言った回数を覚えているのか
PS> $tweets | foreach {$count = 0} { $count += ($_.text -split "寒い").Length - 1} {$count} 137
覚えてないから数えます。137回か。
数値だけを出力するならこんな感じでシンプルに書けますね。
ランキング
クライアントランキング
PS> $tweets | group @{E = {$_.source -replace "<.+?>"}} -NoElement | sort Count -Descending Count Name ----- ---- 12927 Janetter 11333 web 5230 Azurea for Windows 3060 TweetDeck 1694 Hatena 866 twicca 667 twigadge ...
ここからはいろんなランキングを取得してみます。まずはツイートに使ったTwitterクライアントのランキング。ここでもGroup-Objectを使っています。クライアント名はクライアント配布URLがaタグで含まれているのでそれを-replace演算子で削ったものをグループ化キーとしています。ランキングなので最後はCountで降順ソート。
リプライしたユーザーランキング
PS> $tweets | where {$_.in_reply_to_user_id} | select @{L = "user"; E = {if($_.text -match "^(@[a-zA-Z0-9_]+)"){$matches[1]}}} | group user -NoElement | sort Count -Descending Count Name ----- ---- 807 @xxxxxxxxxx 417 @xxxxxxxxxx 333 @xxxxxxxxxx ...
ランキング系はどれもgroup→sort Countのパターンになるかと思います。リプライツイートはin_reply_to_user_id列にリプライしたユーザーIDが含まれるのでまずはそれでフィルタし、ユーザー名はツイート本文から取ります。ユーザー名は-match演算子を使って正規表現で抽出します。$matches自動変数は連想配列で、[0]にマッチ全体が、[1],[2],...にはサブ式のキャプチャが入ります。ちなみにサブ式に名前を付けてるとキー名が数値ではなくサブ式名となります。
ハッシュタグランキング
PS> $tweets | foreach {[regex]::Matches($_.text, "(#\S+)") | % {$_.Captures} |% {$_.Value}} | group -NoElement | where Count -gt 1 | sort Count -Descending Count Name ----- ---- 199 #zanmai 75 #nowplaying 68 #nhk 63 #techedj2009 ...
ハッシュタグも同様のアプローチで取れますが、ハッシュタグは1ツイートに複数あることがあり、-match演算子だと複数のマッチは取れないので[regex]を使って取得しています。
おわりに
PowerShellのオブジェクト処理用コマンドレットを用いると、CSVデータの分析ができます。普通はログファイル等を解析するのに使うわけですが、こういう身近なデータを扱ってみるのも面白いんじゃないでしょうか。きっとPowerShellの勉強にもなると思います。
Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー