pythonのthisモジュールに定義されている変数について

以前紹介した The Zen of Python の記事の中で、
thisというモジュールに仕掛けられたEaster Eggを紹介しました。

その記事中ではインポートした瞬間に表示される文字列にしか焦点を当ててませんでしたが、
このモジュールに含まれている関数や定数についてもちょっと面白いので紹介します。

このthisの中で、どんな変数やメソッドが定義されているのか、dir関数で見てみましょう。


import this
# 略

dir(this)
'''
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'c',
 'd',
 'i',
 's']
'''

“__”で始まる特別な値以外に、c, d, i, s の4つの変数が含まれています。
これらのうち、iとcはそれぞれ整数ですがあまり意味はありません。
ちょっと面白いのは、dとsです。

まず、 dの方は次の辞書です。


print(this.d)
# 以下出力
{'A': 'N', 'B': 'O', 'C': 'P', 'D': 'Q', 'E': 'R', 'F': 'S', 'G': 'T', 'H': 'U', 'I': 'V', 'J': 'W', 'K': 'X', 'L': 'Y', 'M': 'Z', 'N': 'A', 'O': 'B', 'P': 'C', 'Q': 'D', 'R': 'E', 'S': 'F', 'T': 'G', 'U': 'H', 'V': 'I', 'W': 'J', 'X': 'K', 'Y': 'L', 'Z': 'M', 'a': 'n', 'b': 'o', 'c': 'p', 'd': 'q', 'e': 'r', 'f': 's', 'g': 't', 'h': 'u', 'i': 'v', 'j': 'w', 'k': 'x', 'l': 'y', 'm': 'z', 'n': 'a', 'o': 'b', 'p': 'c', 'q': 'd', 'r': 'e', 's': 'f', 't': 'g', 'u': 'h', 'v': 'i', 'w': 'j', 'x': 'k', 'y': 'l', 'z': 'm'}

そして、 sは次の意味不明な文字列が入ってます。


print(this.s)
Gur Mra bs Clguba, ol Gvz Crgref

Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!

ただこの文字列、薄目にみてみると、各単語の文字数が The Zen of Python が似ています。

実はこれはちょっとした暗号になっていて、
this.s の各文字を、this.dの辞書で変換すると、 The Zen of Python が現れます。

書き方はいろいろあると思いますが次のような形でやってみましょう。


print("".join(map(lambda x: this.d.get(x, x), this.s)))
# 以下出力
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

モジュール this の実装自体が
The Zen of Python に反してややこしい値になっているというちょっとした遊び心のようです。

ちなみに、 this モジュールのソースコードも読んでみたのですが、 mapではなく、内包表記でやっているようですね。
sやdを定義したあとに、次のように書いてありました。
よく考えなくてもこれで十分ですね。


print("".join([d.get(c, c) for c in s]))

DataFrameのsampleメソッドのドキュメントを読む

超高頻度で使っているメソッドなのに、公式ドキュメントを読んだことがなかった、
pandas.DataFrame.sample
についてドキュメンを読んでみました。

元々の目的はデータ件数が不明なデータフレームからn個のサンプルが欲しい時に、
df.sample(n)とすると、データフレームの件数が少ないとエラーになるのが面倒だし、
事前にlen(df)して、条件分岐するのが面倒なので都合のいいオプションを探していました。


df.sample(200)
# ValueError: Cannot take a larger sample than population when 'replace=False'

これを回避するために、いつも何かしら工夫をしていますが、正直無駄に行数が増えてる気がしています。
(コードのイメージ)


n = 200
if len(df) >= n:
    df.sample(n)
else:
    df

dfの行数がnより大きいならn件返して欲しくて、行数が少ないなら全部のデータをそのまま渡すというのを、
if文を使わずにsample()の引数で実現したかったわけです。
(結論からいうと、そのような引数は用意されていませんでした。)

エラーメッセージの指示に従ってreplace=True を設定すると、
同じ行が複数回サンプリングされるのを許すのでnが行数より大きくても大丈夫になります。
(ただ、これは自分が元々やりたかったのとは違う。)

dfを150行のデータフレームとすると次のような感じ。


print(len(df))  # 150
print(len(df.sample(200, replace=True)))  # 200

目的のオプションは見つかりませんでしたが、その代わり、今まで知らなかった引数が使えることを知りました。
正確なステートメントは公式ドキュメントの方に任せて、ざっくりと書くと以下のようなものが使えます。

frac : サンプリングするデータの数を、個数ではなく割合で指定する。
replace : Trueにすると、同じデータを重複してサンプリングできる。
weights : サンプリングされる確率を重み付けできます。
random_state : 乱数固定。
axis : 1を指定すると、行ではなく列をサンプリングできる。

決定木系の機械学習モデルを自分で実装するときなどに便利そうですね。

matplotlibのグラフ間の間隔の調整

普段の分析でもこのブログでも、matplotlibの一つのfigureに複数のグラフを描写することがよくあります。

その時、稀に困るのがタイトルやラベルが他のグラフに重なってしまう時です。
それ以外にも微妙に感覚が詰まりすぎてたり広すぎたりで見た目が悪いなぁと思うことがあります。

その場合、subplots_adjustを使って、間隔を調整できます。
ドキュメントはこちら。
matplotlib.pyplot.subplots_adjust
引数の意味はこちらにもあります。
matplotlib.figure.SubplotParams

次の6種類の引数を渡せますが、間隔の調整に使うのは、hspaceとwspaceです。

The parameter meanings (and suggested defaults) are:

left = 0.125 # the left side of the subplots of the figure
right = 0.9 # the right side of the subplots of the figure
bottom = 0.1 # the bottom of the subplots of the figure
top = 0.9 # the top of the subplots of the figure
wspace = 0.2 # the amount of width reserved for space between subplots,
# expressed as a fraction of the average axis width
hspace = 0.2 # the amount of height reserved for space between subplots,
# expressed as a fraction of the average axis height
Copy to clipboard
The actual defaults are controlled by the rc file

無理やり作った例で恐縮ですが、調整せずにグラフ間で重なってしまった例と、
subplots_adjustで調整した例を作りました。


import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [1, 10, 100, 1000, 10000]
# 悪い例
fig = plt.figure(figsize=(9, 6),  facecolor="white")
for i in range(3):
    for j in range(3):
        ax = fig.add_subplot(3, 3, 1 + j + 3*i)
        ax.set_title("複数行の\nタイトル")
        ax.plot(x, y)
plt.show()
# 間隔を調整した例
fig = plt.figure(figsize=(9, 6),  facecolor="white")
fig.subplots_adjust(hspace=0.6, wspace=0.4)
for i in range(3):
    for j in range(3):
        ax = fig.add_subplot(3, 3, 1 + j + 3*i)
        ax.set_title("複数行の\nタイトル")
        ax.plot(x, y)
plt.show()

出力はそれぞれこちらです。

hspace=0.6 と wspace=0.4 の値ですが、これはドキュメント読んで何かしら計算して決めるより、
適当な値を入れて何度か試すのがおすすめです。
初期値はそれぞれ0.2なので、それより大きい値を入れると広くなります。

スタージェスの公式によるヒストグラムのビンの数の決定

データの傾向を見るときにヒストグラムを描くことは頻繁にありますが、
そのとき課題になるのが、ビンの数を何本にするかです。

いつもmatplotlibのデフォルトである10本でとりあえず試したり、
適当に変えながら何パターンか試したりしています。
Tableauであれば、本数ではなく幅での指定ですね。

ただ、何かしら参考指標が欲しいとは思っていたので調べたところ、複数の方法が提案されていました、
その中でスタージェスの公式(Sturges’ formula)が良さそうだったので試してみました。

定義はwikipediaのものを採用しましょう。
(他のサイトを見ると、小数点以下の扱いで微妙に異なるパターンがあります。)
ヒストグラム – Wikipedia

スタージェスの公式によると、n個のデータがあるとき、ビンの数kの目安は次の式で得られます。

$$k = \lceil \log_{2}{n} + 1\rceil$$

注意として、スタージェスの公式はその導出の背景に、二項分布が正規分布で近似できるという性質を使っています。
そのため、nが小さすぎる場合にはあまり参考になりません。
(そのような時はそもそも、ヒストグラム自体があまり有効ではないです。)
また、 データの分布が二項分布/正規分布と大きく異なる時もうまくいきません。

それではいくつかのデータで試してみましょう。
次のコードは、7種類の件数に対して、ランダムにデータを取得し、
スタージェスの公式で得られたビンの数ののヒストグラム(中央列)と、
それよりビンが2本少ないヒストグラム(左列)、2本多いヒストグラム(右列)を描写します。


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


def sturges_formula(data_size):
    return int(np.floor(1+np.log2(data_size)))


fig = plt.figure(figsize=(15, 28), facecolor="white")
for i in range(7):
    data_size = int(1.5 * 2 ** (i+4))
    data = np.random.randn(data_size)
    bin_count = sturges_formula(data_size)

    for j in range(3):
        ax = fig.add_subplot(7, 3, 3*i+j+1)
        ax.set_title("データ件数:{d}件, ビン:{b}本".format(
            d=data_size, b=bin_count-2+2*j
        ))
        ax.hist(data, bins=bin_count-2+2*j, rwidth=0.8)

plt.show()

乱数を使っているので毎回結果は変わりますが、出力の一例がこちら。

左列は確かに若干少ないかなという気がします。
ただ、データ件数が多くなると、もう少し多い数(右列)でもいいので、やはり目安として使うのが良さそうですね。

pandas DataFrameをエクセルで開いても文字化けしないcsvファイルに書き出す

pandasのDataFrameをファイルに保存したい時、
to_csvやto_excelなど、とても便利なメソッドが用意されています。

pandas.DataFrame.to_csv
pandas.DataFrame.to_excel

ただ、日本語文字を含むデータフレームをto_csvで書き出すと、
それをエクセルで開いたときに文字化けしてしまいます。
(他職種のメンバーに連携するとき非常に厄介な現象です。)

エクセルで開きたいなら to_excel() を使えばいいので、普段はそれでクリアしているのですが、
データ量やその後の用途の問題で、csvファイルで要求されるが、その人はエクセルでも使うといったパターンがあります。

このような時は、 encoding="utf-8_sig"を指定することで文字化けしないようにできます。
(pandasにとってExcelはスコープ外だからか、公式ドキュメントにはこの辺りのことを明記している記述は見つかりませんでした)


import pandas as pd
df = pd.DataFrame(
        {
            'col1': ["あ", "い", "う", "え", "お"],
            'col2': ["か", "き", "く", "け", "こ"],
        }
    )
df.to_csv("export_1.csv", index=None)
df.to_csv("export_2.csv", encoding="utf-8_sig", index=None)

# jupyter で実行しているので、!をつけることでBashコマンドを実行
!ls -la *.csv
-rw-r--r--@ 1 yutaro  staff  50  5 25 01:25 export_1.csv
-rw-r--r--@ 1 yutaro  staff  53  5 25 01:25 export_2.csv

2つのファイルを見比べると、 encoding=”utf-8_sig” をつけた方は 3バイトファイルサイズが大きいことがわかります。
これは、BOM(バイトオーダーマーク)と呼ばれる情報が付加されたためで、
これによって、 export_2.csv の方はExcelで開いても文字化けしなくなります。
(export_1.csv は文字化けします。)

matplotlibでバイオリン図

データの可視化方法として、バイオリン図というものがあります。
バイオリン図 – Wikipedia

pythonでこれを書こうと思ったら、 Seaborn を使うのが定番なのですが、
実はmatplotlibにもAPIが用意されていたことを知ったのでその紹介です。

ドキュメント
matplotlib.axes.Axes.violinplot
(ただ、どうも Seaborn の方が使いやすそう。)

とりあえず動かしてみましょう。データは手頃なところで、 iris の4つの特徴量を可視化してみます。


import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
data = load_iris().data
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, title="iris data")
ax.violinplot(data)
plt.show()

出力はこちら。

先ほどのドキュメントに記載されている通り、他にも細かなオプションで見た目が調整できるのですが、
Seaborn の方が多機能のようです。
バイオリンの左右に違う分布を表示するといったことを行いたかったのですが、
どうやらmatplotlibではできなさそう。

MeCabの出力フォーマット

しばらくふれない間にまたど忘れしてしまったので、MeCabの出力フォーマットのメモです。

公式サイトの使い方 に書いてあります。

表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

よくある例の「すもももももももものうち」を形態素解析してみても * が多くてわかりにくいので、
先の公式ページの説明を読んだ方が早いです。


~$ mecab
すもももももももものうち
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

一通り例が見たい場合、*になってばかりでなかなか登場しないのが品詞細分類3ですが、
文章に地名や人名が含まれている時に登場するので、次のような文章はいかがでしょうか。


~$ mecab
渋谷で働くデータサイエンティスト
渋谷	名詞,固有名詞,地域,一般,*,*,渋谷,シブヤ,シブヤ
で	助詞,格助詞,一般,*,*,*,で,デ,デ
働く	動詞,自立,*,*,五段・カ行イ音便,基本形,働く,ハタラク,ハタラク
データ	名詞,一般,*,*,*,*,データ,データ,データ
サイエンティスト	名詞,一般,*,*,*,*,サイエンティスト,サイエンティスト,サイエンティスト
EOS

自分は表層形、品詞、原形をよく使うので、
‘\t’でsplitした結果の、index 0 が、表層形、
index 1をさらに’,’ split して、 index 0 が品詞、index 6が原型、
ってのだけ把握していればなんとかなることが多いです。

それで、適当なテキストを試しに形態素解析して、
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
と出てきて、何番目が原型だ?? となって公式サイト見に行く、ということを何度もやらかしています。

pipでライブラリのバージョンを指定してインストール

pipでライブラリのバージョンを気軽にあげていたら、動かなくなるものが出たので戻し作業をやりました。
良い機会なので、pipで指定したバージョンのライブラリをインストールする方法を紹介します。

方法は、ドキュメントの、examplesの中にあるように、ライブラリ名の後ろに==でつなげてバージョンを指定するだけです。
pip install – examples

ちなみに、今回動かなくなったのは statsmodels です。(バージョンを上げたのはscipy)


# 動かなくなったコード。 ただのインポートさえできなくなった。
import statsmodels.api as sm

# 出力されたエラー
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
 in ()
      1 import numpy as np
      2 import matplotlib.pyplot as plt
----> 3 import statsmodels.api as sm

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/api.py in ()
     14 from . import robust
     15 from .robust.robust_linear_model import RLM
---> 16 from .discrete.discrete_model import (Poisson, Logit, Probit,
     17                                       MNLogit, NegativeBinomial,
     18                                       GeneralizedPoisson,

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/discrete/discrete_model.py in ()
     43 
     44 from statsmodels.base.l1_slsqp import fit_l1_slsqp
---> 45 from statsmodels.distributions import genpoisson_p
     46 
     47 try:

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/distributions/__init__.py in ()
      1 from .empirical_distribution import ECDF, monotone_fn_inverter, StepFunction
----> 2 from .edgeworth import ExpandedNormal
      3 from .discrete import genpoisson_p, zipoisson, zigenpoisson, zinegbin

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/distributions/edgeworth.py in ()
      5 import numpy as np
      6 from numpy.polynomial.hermite_e import HermiteE
----> 7 from scipy.misc import factorial
      8 from scipy.stats import rv_continuous
      9 import scipy.special as special

ImportError: cannot import name 'factorial'

statsmodelsがインポートできないので、statsmodelsを疑ってしまいそうですが、
根本的な原因は、scipyから、 factorialをインポートできなかったことでした。

----> 7 from scipy.misc import factorial

最近、scipyのバージョンを 1.1.0から 1.3.0 へ上げたのが怪しいです。
真の原因は何か他にありそうな気もするのですが、とりあえずscipy のバージョンを戻します。

戻し手順は最初に対象のライブラリをアンインストールした後に、再度バージョン指定してインストールです。


$ pip uninstall scipy
Uninstalling scipy-1.3.0:
# --- 略 ---
Proceed (y/n)? y
  Successfully uninstalled scipy-1.3.0

$ pip install scipy==1.1.0
# --- 略 ---
Installing collected packages: scipy

これでscipyのバージョンが戻り、再びstatsmodelsをimport できるようになりました。

pandasのデータフレームから重複行を削除する

前回の記事が重複行の抽出だったので今回は重複行の削除です。
参考:pandasのデータフレームの重複行を抽出する

このあと専用の関数を紹介しますが、
重複行の抽出条件を反転させればできてしまうので、この書き方でも構わないと思います。
print(df[~df.duplicated()])
(むしろ、duplicated関数の引数keepの意味はこちらの方がわかりやすい。)


import pandas as pd
import numpy as np

# データの作成
df = pd.DataFrame(
        {
            "col0": np.random.choice(["A", "B", "C"], size=10),
            "col1": np.random.choice(["a", "b", "c"], size=10),
            "col2": np.random.choice(["1", "2", "3"], size=10),
        }
    )
# データの確認
print(df)
'''
  col0 col1 col2
0    C    a    1
1    B    a    2
2    A    c    3
3    B    c    1
4    B    a    2
5    C    a    1
6    C    b    2
7    B    a    2
8    C    c    3
9    C    c    3
'''
# duplicated()の結果を反転させると重複していない行を抽出できる。
print(df[~df.duplicated()])
'''
  col0 col1 col2
0    C    a    1
1    B    a    2
2    A    c    3
3    B    c    1
6    C    b    2
8    C    c    3
'''

pandasには重複行削除専用のメソッドとして、drop_duplicatesが定義されています。
ドキュメントはこちら。
pandas.DataFrame.drop_duplicates

引数の keep と subset は duplicated と同じように使えます。
また、inplace に Trueを指定することで、そのデータフレームの値自体を書き換えます。
デフォルトはFalseで、重複行を削除した結果を返します。


print(df.drop_duplicates())
'''
  col0 col1 col2
0    C    a    1
1    B    a    2
2    A    c    3
3    B    c    1
6    C    b    2
8    C    c    3
'''

先ほどの結果と同じもののが得られましたね。

pandasの実装を見るとわかるのですが、ただのラッパーなので、
とくにこちらの方が実行速度が速いといったことはなさそうです。
https://github.com/pandas-dev/pandas/blob/v0.24.2/pandas/core/frame.py#L4605-L4637

pandasのデータフレームの重複行を抽出する

データフレームに入ったデータについて、重複がないか確認したい場面は結構あると思います。
僕の場合は、そういう時はSQLでデータを抽出する段階でユニークにしてしまうことが多いです。
また、特定の列の値(idなど)の一意性の確認であれば、value_counts()して、2個以上含まれている値が無いか見たりします。
それ以外のケースでも、groupbyやsetなど使ってゴリゴリコードを書いてどうにかすることが多いのですが、
せっかく専用の関数が用意されているので今後はそれを使うようにしたいです。

ということで、今回はduplicated()の紹介です。
ドキュメントはこちら。
pandas.DataFrame.duplicated

pythonを勉強し始めた頃、引数なしで実行して、
出力が直感的でなくて使いにくいなーと思っていたのですが、subsetやkeepなどの引数をきちんと理解すれば非常に便利です。

とりあえず実験するために、ダミーデータを作っておきます。


import pandas as pd
import numpy as np

df = pd.DataFrame(
        {
            "col0": np.random.choice(["A", "B", "C"], size=10),
            "col1": np.random.choice(["a", "b", "c"], size=10),
            "col2": np.random.choice(["1", "2", "3"], size=10),
        }
    )

print(df)
'''
出力 (乱数を使っているので実行するたびに変わります。)
  col0 col1 col2
0    B    b    1
1    C    c    2
2    A    b    1
3    B    a    2
4    A    b    1
5    C    b    3
6    A    b    1
7    B    b    1
8    A    b    3
9    B    b    3
'''

このデータフレームに対して、引数無しで実行したものがこちらです。


print(df.duplicated())
'''
0    False
1    False
2    False
3    False
4     True
5    False
6     True
7     True
8    False
9    False
dtype: bool
'''

これ、一見、インデックス4,5,6の行が重複してるという意味に勘違いしそうなのですがそうではありません。
引数を省略すると、keep="first"を指定したことになり、重複行のうち、最初の値はFalseになります。
なので、インデックス4の行と重複してる行が0~3の間にあるという意味です。(実際はインデックス2)

keep="last"を指定すると、逆に重複している行のうち、
最後の行はFalse、それ以外の行はTrueになります。(重複してない行はFalse)
こちらの方が、時系列に並べたデータのうち、最新のデータを取りたい場面などで重宝すると思います。
また、 subsetに列名の集合を渡すと、その列だけを使って判定を行います。
これもまとめて試してみましょう。


print(df.duplicated(subset=["col1", "col2"], keep="last"))
'''
0     True
1    False
2     True
3    False
4     True
5     True
6     True
7    False
8     True
9    False
dtype: bool
'''

重複した行全部を抽出したい、という場合は、keep=Falseを指定します。
また、先に紹介した2例でもそうなのですが、実際に使う時は重複した行のインデックスではなくて、
その行そのものをみたいという場面が多いので、次のような使い方になると思います。


print(df[df.duplicated(subset=["col1", "col2"], keep=False)])
'''
  col0 col1 col2
0    B    b    1
2    A    b    1
4    A    b    1
5    C    b    3
6    A    b    1
7    B    b    1
8    A    b    3
9    B    b    3
'''

df[]の中に放り込んであげるだけですね。
コードのテスト時など、無いはずの重複が本当に無いことを確認する時などは、
duplicated関数は結構便利です。

重複を除外する時などはまた専用の関数があるので次に紹介します。